├── .editorconfig ├── .eslintrc.cjs ├── .gitattributes ├── .github ├── .kodiak.toml ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── fossa.yml │ ├── labeler.yml │ ├── release-please.yml │ └── workflow.yml ├── .gitignore ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── codecov.yml ├── commitlint.config.cjs ├── package-lock.json ├── package.json ├── renovate.json5 ├── src ├── all.js ├── conditions.js ├── index.js ├── line_parser.js ├── merge.js ├── netlify_config_parser.js ├── normalize.js ├── results.js ├── status.js └── url.js └── tests ├── all.js ├── fixtures ├── netlify_config │ ├── backward_compat_destination.toml │ ├── backward_compat_origin.toml │ ├── backward_compat_parameters.toml │ ├── backward_compat_params.toml │ ├── backward_compat_sign.toml │ ├── backward_compat_signing.toml │ ├── complex.toml │ ├── conditions_country_case.toml │ ├── conditions_language_case.toml │ ├── conditions_role_case.toml │ ├── empty.toml │ ├── from_forward.toml │ ├── from_no_slash.toml │ ├── from_simple.toml │ ├── from_url.toml │ ├── invalid_dot_netlify_path.toml │ ├── invalid_dot_netlify_url.toml │ ├── invalid_forward_status.toml │ ├── invalid_headers.toml │ ├── invalid_no_from.toml │ ├── invalid_no_to.toml │ ├── invalid_object.toml │ ├── invalid_status_empty.toml │ ├── invalid_status_high.toml │ ├── invalid_status_negative.toml │ ├── invalid_status_string.toml │ ├── invalid_toml.toml │ ├── invalid_type.toml │ ├── invalid_url.toml │ ├── minimal.toml │ ├── query.toml │ ├── signed.toml │ └── status_string.toml └── redirects_file │ ├── comment_full │ ├── comment_inline │ ├── conditions_country │ ├── conditions_country_case │ ├── conditions_country_language │ ├── conditions_language_case │ ├── conditions_query │ ├── conditions_role │ ├── conditions_role_case │ ├── conditions_roles │ ├── empty │ ├── empty_line │ ├── from_absolute_uri │ ├── from_simple │ ├── invalid_dir │ └── .gitkeep │ ├── invalid_dot_netlify_path │ ├── invalid_dot_netlify_url │ ├── invalid_mistaken_headers │ ├── invalid_no_slash │ ├── invalid_no_to_no_status │ ├── invalid_no_to_query │ ├── invalid_no_to_status │ ├── invalid_status │ ├── invalid_url │ ├── line_trim │ ├── multiple_lines │ ├── proxy │ ├── query │ ├── signed │ ├── signed_backward_compat │ ├── simple │ ├── simple_multiple │ ├── status │ ├── status_force │ ├── to_anchor │ ├── to_path_forward │ ├── to_splat_force │ └── to_splat_no_force ├── helpers └── main.js ├── line_parser.js ├── merge.js └── netlify_config_parser.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | max_line_length = 120 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | const { overrides } = require('@netlify/eslint-config-node/.eslintrc_esm.cjs') 2 | 3 | module.exports = { 4 | extends: ['plugin:fp/recommended', '@netlify/eslint-config-node/.eslintrc_esm.cjs'], 5 | rules: { 6 | 'fp/no-mutation': [2, { commonjs: true }], 7 | }, 8 | overrides: [...overrides], 9 | } 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/.kodiak.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [merge.automerge_dependencies] 4 | versions = ["minor", "patch"] 5 | usernames = ["renovate"] 6 | 7 | [approve] 8 | auto_approve_usernames = ["renovate"] -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @netlify/workflow 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 🎉 Thanks for submitting a pull request! 🎉 2 | 3 | #### Summary 4 | 5 | Fixes # 6 | 7 | 10 | 11 | --- 12 | 13 | For us to review and ship your PR efficiently, please perform the following steps: 14 | 15 | - [ ] Open a [bug/issue](https://github.com/netlify/netlify-redirect-parser/issues/new/choose) before writing your code 16 | 🧑‍💻. This ensures we can discuss the changes and get feedback from everyone that should be involved. If you\`re 17 | fixing a typo or something that\`s on fire 🔥 (e.g. incident related), you can skip this step. 18 | - [ ] Read the [contribution guidelines](../CONTRIBUTING.md) 📖. This ensures your code follows our style guide and 19 | passes our tests. 20 | - [ ] Update or add tests (if any source code was changed or added) 🧪 21 | - [ ] Update or add documentation (if features were changed or added) 📝 22 | - [ ] Make sure the status checks below are successful ✅ 23 | 24 | **A picture of a cute animal (not mandatory, but encouraged)** 25 | -------------------------------------------------------------------------------- /.github/workflows/fossa.yml: -------------------------------------------------------------------------------- 1 | name: Dependency License Scanning 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - chore/fossa-workflow 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | jobs: 14 | fossa: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | - name: Download fossa cli 20 | run: |- 21 | mkdir -p $HOME/.local/bin 22 | curl https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash -s -- -b $HOME/.local/bin 23 | echo "$HOME/.local/bin" >> $GITHUB_PATH 24 | 25 | - name: Fossa init 26 | run: fossa init 27 | - name: Set env 28 | run: echo "line_number=$(grep -n "project" .fossa.yml | cut -f1 -d:)" >> $GITHUB_ENV 29 | - name: Configuration 30 | run: |- 31 | sed -i "${line_number}s|.*| project: git@github.com:${GITHUB_REPOSITORY}.git|" .fossa.yml 32 | cat .fossa.yml 33 | - name: Upload dependencies 34 | run: fossa analyze --debug 35 | env: 36 | FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} 37 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: Label PR 2 | on: 3 | pull_request: 4 | types: [opened, edited] 5 | 6 | jobs: 7 | label-pr: 8 | if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | pr: 13 | [ 14 | { prefix: 'fix', type: 'bug' }, 15 | { prefix: 'chore', type: 'chore' }, 16 | { prefix: 'test', type: 'chore' }, 17 | { prefix: 'ci', type: 'chore' }, 18 | { prefix: 'feat', type: 'feature' }, 19 | { prefix: 'security', type: 'security' }, 20 | ] 21 | steps: 22 | - uses: netlify/pr-labeler-action@v1.1.0 23 | if: startsWith(github.event.pull_request.title, matrix.pr.prefix) 24 | with: 25 | token: '${{ secrets.GITHUB_TOKEN }}' 26 | label: 'type: ${{ matrix.pr.type }}' 27 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: navikt/github-app-token-generator@a9cd374e271b8aef573b8c16ac46c44fb89b02db 11 | id: get-token 12 | with: 13 | private-key: ${{ secrets.TOKENS_PRIVATE_KEY }} 14 | app-id: ${{ secrets.TOKENS_APP_ID }} 15 | - uses: GoogleCloudPlatform/release-please-action@v3 16 | id: release 17 | with: 18 | token: ${{ steps.get-token.outputs.token }} 19 | release-type: node 20 | package-name: 'netlify-redirect-parser' 21 | - uses: actions/checkout@v3 22 | if: ${{ steps.release.outputs.release_created }} 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: '*' 26 | cache: 'npm' 27 | check-latest: true 28 | registry-url: 'https://registry.npmjs.org' 29 | if: ${{ steps.release.outputs.release_created }} 30 | - run: npm publish 31 | if: ${{ steps.release.outputs.release_created }} 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 34 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | # Ensure GitHub actions are not run twice for same commits 4 | push: 5 | branches: [main] 6 | tags: ['*'] 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macOS-latest, windows-latest] 14 | node-version: [12.20.0, '*'] 15 | exclude: 16 | - os: macOS-latest 17 | node-version: 12.20.0 18 | - os: windows-latest 19 | node-version: 12.20.0 20 | fail-fast: false 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - name: Git checkout 24 | uses: actions/checkout@v3 25 | - name: Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'npm' 30 | check-latest: true 31 | - name: Install dependencies 32 | run: npm ci 33 | - name: Linting 34 | run: npm run format:ci 35 | if: "${{ matrix.node-version == '*' }}" 36 | - name: Tests 37 | run: npm run test:ci 38 | - name: Get test coverage flags 39 | id: test-coverage-flags 40 | run: |- 41 | os=${{ matrix.os }} 42 | node=${{ matrix.node-version }} 43 | echo "::set-output name=os::${os/-latest/}" 44 | echo "::set-output name=node::node_${node//[.*]/}" 45 | shell: bash 46 | - uses: codecov/codecov-action@v2 47 | with: 48 | file: coverage/coverage-final.json 49 | flags: ${{ steps.test-coverage-flags.outputs.os }},${{ steps.test-coverage-flags.outputs.node }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .eslintcache 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@netlify/eslint-config-node/.prettierrc.json" 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 9 | 10 | ### [13.0.5](https://github.com/netlify/netlify-redirect-parser/compare/v13.0.4...v13.0.5) (2022-02-28) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * remove feature flag `redirects_parser_normalize_status` ([4d5036f](https://github.com/netlify/netlify-redirect-parser/commit/4d5036f88d6a233f3d2a77b824676af717bf8f77)) 16 | 17 | ### [13.0.4](https://github.com/netlify/netlify-redirect-parser/compare/v13.0.3...v13.0.4) (2022-02-24) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * add feature flags to new status normalization logic ([4ad3aad](https://github.com/netlify/netlify-redirect-parser/commit/4ad3aadfdfb42af2bffe773a9da3ecb4696187c9)) 23 | 24 | ### [13.0.3](https://github.com/netlify/netlify-redirect-parser/compare/v13.0.2...v13.0.3) (2022-02-21) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * normalize and validate status code ([970d05e](https://github.com/netlify/netlify-redirect-parser/commit/970d05eb2457600fcb88fb8559f76a951d648386)) 30 | 31 | ### [13.0.2](https://github.com/netlify/netlify-redirect-parser/compare/v13.0.1...v13.0.2) (2022-02-07) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * **deps:** update dependency filter-obj to v3 ([5095fa3](https://github.com/netlify/netlify-redirect-parser/commit/5095fa387c2a40b7d133e28ec852a0b9403d0825)) 37 | * **deps:** update dependency is-plain-obj to v4 ([f7aa65e](https://github.com/netlify/netlify-redirect-parser/commit/f7aa65ebc629fd5a0f72e869b4a24dcd408e394a)) 38 | * **deps:** update dependency path-exists to v5 ([d91bf43](https://github.com/netlify/netlify-redirect-parser/commit/d91bf43f7b67579a405fa07e688bfaca0f42e6ef)) 39 | 40 | ### [13.0.1](https://github.com/netlify/netlify-redirect-parser/compare/v13.0.0...v13.0.1) (2022-01-17) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * husky binary not found ([923e648](https://github.com/netlify/netlify-redirect-parser/commit/923e6487bf25612d4c4f987565eb4f85f6b62083)) 46 | 47 | ## [13.0.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v12.0.0...v13.0.0) (2021-12-02) 48 | 49 | 50 | ### ⚠ BREAKING CHANGES 51 | 52 | * use pure ES modules 53 | 54 | ### Miscellaneous Chores 55 | 56 | * use pure ES modules ([cf540fc](https://www.github.com/netlify/netlify-redirect-parser/commit/cf540fc9f705fd2d7586c3a9eba228dc64e9d268)) 57 | 58 | ## [12.0.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v11.0.3...v12.0.0) (2021-11-25) 59 | 60 | 61 | ### ⚠ BREAKING CHANGES 62 | 63 | * drop support for Node 10 64 | 65 | ### Miscellaneous Chores 66 | 67 | * drop support for Node 10 ([7e4dd35](https://www.github.com/netlify/netlify-redirect-parser/commit/7e4dd359562d3fa48c2681c6077afb59728146f7)) 68 | 69 | ### [11.0.3](https://www.github.com/netlify/netlify-redirect-parser/compare/v11.0.2...v11.0.3) (2021-11-03) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * **parser:** fixes an issue where the country code in the redirects where lowercased ([92a65f1](https://www.github.com/netlify/netlify-redirect-parser/commit/92a65f1b40336917510d71c6ff4c40759483b7a3)) 75 | 76 | ### [11.0.2](https://www.github.com/netlify/netlify-redirect-parser/compare/v11.0.1...v11.0.2) (2021-08-16) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * handle `_redirects` with wrong file type ([a4fdad7](https://www.github.com/netlify/netlify-redirect-parser/commit/a4fdad7c04a1012d89a9e78e54013ed1e1c0227e)) 82 | 83 | ### [11.0.1](https://www.github.com/netlify/netlify-redirect-parser/compare/v11.0.0...v11.0.1) (2021-08-16) 84 | 85 | 86 | ### Bug Fixes 87 | 88 | * **deps:** update dependency is-plain-obj to v3 ([c291468](https://www.github.com/netlify/netlify-redirect-parser/commit/c2914687b06693a3e557a3ea60632d88bd7796e9)) 89 | 90 | ## [11.0.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v10.1.0...v11.0.0) (2021-08-13) 91 | 92 | 93 | ### ⚠ BREAKING CHANGES 94 | 95 | * simplify exported methods 96 | 97 | ### Features 98 | 99 | * simplify exported methods ([3e44c56](https://www.github.com/netlify/netlify-redirect-parser/commit/3e44c5650eae5e323ebebd2bf0f5d257f244f605)) 100 | 101 | ## [10.1.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v10.0.0...v10.1.0) (2021-08-13) 102 | 103 | 104 | ### Features 105 | 106 | * add `configRedirects` argument to `parseAllRedirects()` ([ca58b69](https://www.github.com/netlify/netlify-redirect-parser/commit/ca58b6912fabfcb845c7c453962762724b6fc2ca)) 107 | 108 | ## [10.0.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v9.1.0...v10.0.0) (2021-08-13) 109 | 110 | 111 | ### ⚠ BREAKING CHANGES 112 | 113 | * add lenient parsing 114 | 115 | ### Features 116 | 117 | * add lenient parsing ([562eb4d](https://www.github.com/netlify/netlify-redirect-parser/commit/562eb4d4fccc050241f51aee5193c694a1e9d205)) 118 | 119 | ## [9.1.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v9.0.0...v9.1.0) (2021-08-12) 120 | 121 | 122 | ### Features 123 | 124 | * remove duplicates ([87a0047](https://www.github.com/netlify/netlify-redirect-parser/commit/87a00477dbf79e46da2c0c767416e2016537cff1)) 125 | 126 | ## [9.0.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v8.2.0...v9.0.0) (2021-08-12) 127 | 128 | 129 | ### ⚠ BREAKING CHANGES 130 | 131 | * drop support for Node 8 132 | 133 | ### Miscellaneous Chores 134 | 135 | * drop support for Node 8 ([5259383](https://www.github.com/netlify/netlify-redirect-parser/commit/525938388e898bc41e6439c186adaa6abfb3b246)) 136 | 137 | ## [8.2.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v8.1.0...v8.2.0) (2021-08-05) 138 | 139 | 140 | ### Features 141 | 142 | * add `minimal` option ([85d2fc6](https://www.github.com/netlify/netlify-redirect-parser/commit/85d2fc684c50accf31ac1d77e5b491fe6a22f70d)) 143 | 144 | ## [8.1.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v8.0.0...v8.1.0) (2021-07-08) 145 | 146 | 147 | ### Features 148 | 149 | * align `parseAllRedirects()` order with `@netlify/config` order ([aec6f8a](https://www.github.com/netlify/netlify-redirect-parser/commit/aec6f8ab8c7a525204b23b04325d0f4bbf304c25)) 150 | 151 | ## [8.0.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v7.0.0...v8.0.0) (2021-06-22) 152 | 153 | 154 | ### ⚠ BREAKING CHANGES 155 | 156 | * fix return value shape 157 | 158 | ### Features 159 | 160 | * fix return value shape ([fb2d8fa](https://www.github.com/netlify/netlify-redirect-parser/commit/fb2d8fa4065ec286b946044aeb5bfb9b2c5b4066)) 161 | 162 | ## [7.0.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v6.0.0...v7.0.0) (2021-06-14) 163 | 164 | 165 | ### ⚠ BREAKING CHANGES 166 | 167 | * fix conditions parsing 168 | 169 | ### Bug Fixes 170 | 171 | * fix conditions parsing ([1057295](https://www.github.com/netlify/netlify-redirect-parser/commit/1057295348f3dc5c1a5bac523ddae67245d1da20)) 172 | 173 | ## [6.0.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v5.2.1...v6.0.0) (2021-06-14) 174 | 175 | 176 | ### ⚠ BREAKING CHANGES 177 | 178 | * rename `query` to `parameters` 179 | 180 | ### Bug Fixes 181 | 182 | * rename `query` to `parameters` ([80c9ae9](https://www.github.com/netlify/netlify-redirect-parser/commit/80c9ae9e77c7abc565cabd0f92ffe4432d143ddb)) 183 | 184 | ### [5.2.1](https://www.github.com/netlify/netlify-redirect-parser/compare/v5.2.0...v5.2.1) (2021-06-14) 185 | 186 | 187 | ### Bug Fixes 188 | 189 | * add `origin` field ([20408e2](https://www.github.com/netlify/netlify-redirect-parser/commit/20408e2377a3b7abbd704c5d6303484a289916e1)) 190 | 191 | ## [5.2.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v5.1.1...v5.2.0) (2021-06-14) 192 | 193 | 194 | ### Features 195 | 196 | * improve error message ([7b3042e](https://www.github.com/netlify/netlify-redirect-parser/commit/7b3042e5f94a03fe26c3aa29d47acf8ac5a6b121)) 197 | 198 | ### [5.1.1](https://www.github.com/netlify/netlify-redirect-parser/compare/v5.1.0...v5.1.1) (2021-06-11) 199 | 200 | 201 | ### Bug Fixes 202 | 203 | * handle invalid `redirects` syntax in `netlify.toml` ([e998366](https://www.github.com/netlify/netlify-redirect-parser/commit/e998366104efe0c68d674b7b17e0dfb53e6025eb)) 204 | 205 | ## [5.1.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v5.0.0...v5.1.0) (2021-06-10) 206 | 207 | 208 | ### Miscellaneous Chores 209 | 210 | * release 5.1.0 ([0b8f903](https://www.github.com/netlify/netlify-redirect-parser/commit/0b8f90324b5ab98299c82bec4a60022de3b3c2f1)) 211 | 212 | ## [5.0.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v4.2.0...v5.0.0) (2021-06-10) 213 | 214 | 215 | ### Features 216 | 217 | * change the main exported steps ([e33d76f](https://www.github.com/netlify/netlify-redirect-parser/commit/e33d76f57a6ec4dc13c49006a518ff43c0c141d8)) 218 | 219 | 220 | ### Miscellaneous Chores 221 | 222 | * release 5.0.0 ([9d9c0bf](https://www.github.com/netlify/netlify-redirect-parser/commit/9d9c0bfdd0919cb4f4683285331ebcb8ca2772dc)) 223 | 224 | ## [4.2.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v4.1.0...v4.2.0) (2021-06-10) 225 | 226 | 227 | ### Miscellaneous Chores 228 | 229 | * release 4.2.0 ([2a944bc](https://www.github.com/netlify/netlify-redirect-parser/commit/2a944bc6001fab8a2158f615d44fc4d522701f1e)) 230 | 231 | ## [4.1.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v4.0.5...v4.1.0) (2021-06-10) 232 | 233 | 234 | ### Features 235 | 236 | * allow `_redirects` to be optional ([9283ed9](https://www.github.com/netlify/netlify-redirect-parser/commit/9283ed9a5d363810ff432b2f6022b9a974ff17dd)) 237 | 238 | ### [4.0.5](https://www.github.com/netlify/netlify-redirect-parser/compare/v4.0.4...v4.0.5) (2021-06-09) 239 | 240 | 241 | ### Bug Fixes 242 | 243 | * **deps:** update dependency @netlify/config to ^6.9.2 ([f511936](https://www.github.com/netlify/netlify-redirect-parser/commit/f511936f897f6fd73d06abda6a090fd473e679f6)) 244 | 245 | ### [4.0.4](https://www.github.com/netlify/netlify-redirect-parser/compare/v4.0.3...v4.0.4) (2021-06-09) 246 | 247 | 248 | ### Bug Fixes 249 | 250 | * **deps:** update dependency @netlify/config to ^6.9.1 ([852605b](https://www.github.com/netlify/netlify-redirect-parser/commit/852605b791344a5c8c0f900c497e737388d9d0ea)) 251 | 252 | ### [4.0.3](https://www.github.com/netlify/netlify-redirect-parser/compare/v4.0.2...v4.0.3) (2021-06-09) 253 | 254 | 255 | ### Bug Fixes 256 | 257 | * **deps:** update dependency @netlify/config to ^6.9.0 ([#240](https://www.github.com/netlify/netlify-redirect-parser/issues/240)) ([107f3e4](https://www.github.com/netlify/netlify-redirect-parser/commit/107f3e420e1d7e1c16a34a1377fcee7a45d8a0e1)) 258 | 259 | ### [4.0.2](https://www.github.com/netlify/netlify-redirect-parser/compare/v4.0.1...v4.0.2) (2021-06-08) 260 | 261 | 262 | ### Bug Fixes 263 | 264 | * **deps:** update dependency @netlify/config to ^6.7.3 ([9921e4c](https://www.github.com/netlify/netlify-redirect-parser/commit/9921e4ce5e590a967db2a85ddc4e13b45fe70139)) 265 | 266 | ### [4.0.1](https://www.github.com/netlify/netlify-redirect-parser/compare/v4.0.0...v4.0.1) (2021-06-04) 267 | 268 | 269 | ### Bug Fixes 270 | 271 | * **deps:** update dependency @netlify/config to ^6.7.2 ([656e136](https://www.github.com/netlify/netlify-redirect-parser/commit/656e1362f42cd19531e2c7e7e297eb707eed1c81)) 272 | 273 | ## [4.0.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.29...v4.0.0) (2021-06-02) 274 | 275 | 276 | ### ⚠ BREAKING CHANGES 277 | 278 | * use exceptions 279 | * rename `params` to `query` 280 | * default `params` to an empty object 281 | * ensure `conditions` is always set 282 | * remove browser partial support 283 | * enforce `force` to be a boolean 284 | * enforce `proxy` to be a boolean everywhere 285 | * enforce `proxy` to be a boolean 286 | 287 | ### Features 288 | 289 | * add support for `edge_handlers` ([4c42d34](https://www.github.com/netlify/netlify-redirect-parser/commit/4c42d34c20e08a9a2d2bc2469c3646743674bc47)) 290 | * enforce `force` to be a boolean ([30d3879](https://www.github.com/netlify/netlify-redirect-parser/commit/30d38793de699f396c79d6e9919eba5a5e3eec7b)) 291 | * enforce `proxy` to be a boolean ([5aca6e0](https://www.github.com/netlify/netlify-redirect-parser/commit/5aca6e0ee1c53d470f7066314cecd59eaddc187f)) 292 | * enforce `proxy` to be a boolean everywhere ([ae644a5](https://www.github.com/netlify/netlify-redirect-parser/commit/ae644a53d36e4d0ec0c82908abcfed6dfc139e23)) 293 | * ensure `headers` is always set ([2326ad5](https://www.github.com/netlify/netlify-redirect-parser/commit/2326ad53b8ab16dd1801ae028c58aefe70d13c1f)) 294 | * improve `README.md` ([d282093](https://www.github.com/netlify/netlify-redirect-parser/commit/d282093b3fec652b49b3701f0c9244cd6ecc3f6a)) 295 | * improve error handling ([ad0a399](https://www.github.com/netlify/netlify-redirect-parser/commit/ad0a3992b5c2ae2a5fb4740814c433b5cc797591)) 296 | * use exceptions ([ecc6fbb](https://www.github.com/netlify/netlify-redirect-parser/commit/ecc6fbb43e94014fbc91a4dc4ce7b4e83cac5829)) 297 | * validate that `redirects` is an array ([03e410a](https://www.github.com/netlify/netlify-redirect-parser/commit/03e410a27f1d6a558be3b1976a76a45d544e68bf)) 298 | 299 | 300 | ### Bug Fixes 301 | 302 | * `signed` backward compatibility priority ([72518ef](https://www.github.com/netlify/netlify-redirect-parser/commit/72518ef0890acb8192a47665862ea9eb1009be72)) 303 | * allow non-force forward rules ([c8b0500](https://www.github.com/netlify/netlify-redirect-parser/commit/c8b0500ca9eccee4ba3fd77c4d35e8c3e570c290)) 304 | * linting ([3fa3531](https://www.github.com/netlify/netlify-redirect-parser/commit/3fa35312766aa55264596884ded33441b8f74b7b)) 305 | * validation of `from` ([14a2ce8](https://www.github.com/netlify/netlify-redirect-parser/commit/14a2ce87c6918d1ce27f80401b5e0100ac64b48c)) 306 | 307 | 308 | ### Miscellaneous Chores 309 | 310 | * default `params` to an empty object ([cb13d58](https://www.github.com/netlify/netlify-redirect-parser/commit/cb13d58a4070fa0e22742054c1cb301b7c25f47a)) 311 | * ensure `conditions` is always set ([607a369](https://www.github.com/netlify/netlify-redirect-parser/commit/607a3697efc0fe0e5f00d8c8e3ff92fe9b1a8ba1)) 312 | * remove browser partial support ([388f619](https://www.github.com/netlify/netlify-redirect-parser/commit/388f6191f2c062dfba02942d608404be3c6313ea)) 313 | * rename `params` to `query` ([1cde275](https://www.github.com/netlify/netlify-redirect-parser/commit/1cde275c82f192cca86c52494ca2400bf6703aba)) 314 | 315 | ### [3.0.29](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.28...v3.0.29) (2021-05-27) 316 | 317 | 318 | ### Bug Fixes 319 | 320 | * **deps:** update dependency @netlify/config to ^6.7.1 ([#157](https://www.github.com/netlify/netlify-redirect-parser/issues/157)) ([f4e45a7](https://www.github.com/netlify/netlify-redirect-parser/commit/f4e45a76f83830538f0aa85a10eb9bb68b8d2bdd)) 321 | 322 | ### [3.0.28](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.27...v3.0.28) (2021-05-24) 323 | 324 | 325 | ### Bug Fixes 326 | 327 | * **deps:** update dependency @netlify/config to ^6.7.0 ([#155](https://www.github.com/netlify/netlify-redirect-parser/issues/155)) ([bfd3a6c](https://www.github.com/netlify/netlify-redirect-parser/commit/bfd3a6c1ba6f13350f3e039315e30e03744cfcd9)) 328 | 329 | ### [3.0.27](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.26...v3.0.27) (2021-05-21) 330 | 331 | 332 | ### Bug Fixes 333 | 334 | * **deps:** update dependency @netlify/config to ^6.6.0 ([6695da0](https://www.github.com/netlify/netlify-redirect-parser/commit/6695da07729de66f43821c8bc81a44697c2a0c36)) 335 | 336 | ### [3.0.26](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.25...v3.0.26) (2021-05-12) 337 | 338 | 339 | ### Bug Fixes 340 | 341 | * **deps:** update dependency @netlify/config to ^6.5.0 ([4fcad76](https://www.github.com/netlify/netlify-redirect-parser/commit/4fcad76c6dc62a9f70793827647b332c2d39e30b)) 342 | 343 | ### [3.0.25](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.24...v3.0.25) (2021-05-05) 344 | 345 | 346 | ### Bug Fixes 347 | 348 | * **deps:** update dependency @netlify/config to ^6.4.4 ([d4de7ce](https://www.github.com/netlify/netlify-redirect-parser/commit/d4de7cebd451c1f302932ccb9c7a566de07b9978)) 349 | 350 | ### [3.0.24](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.23...v3.0.24) (2021-05-04) 351 | 352 | 353 | ### Bug Fixes 354 | 355 | * **deps:** update dependency @netlify/config to ^6.4.3 ([d52a20d](https://www.github.com/netlify/netlify-redirect-parser/commit/d52a20de47a9475cd95ccaccdaae27a0e9d5e44a)) 356 | 357 | ### [3.0.23](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.22...v3.0.23) (2021-05-04) 358 | 359 | 360 | ### Bug Fixes 361 | 362 | * **deps:** update dependency @netlify/config to ^6.4.2 ([30c2cf0](https://www.github.com/netlify/netlify-redirect-parser/commit/30c2cf0f0a2bb43bc344bae8db92f6297a7a59d2)) 363 | 364 | ### [3.0.22](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.21...v3.0.22) (2021-05-03) 365 | 366 | 367 | ### Bug Fixes 368 | 369 | * **deps:** update dependency @netlify/config to ^6.4.1 ([109922f](https://www.github.com/netlify/netlify-redirect-parser/commit/109922f9b020acc1ef4b702185dbd290cb8e7cec)) 370 | 371 | ### [3.0.21](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.20...v3.0.21) (2021-05-03) 372 | 373 | 374 | ### Bug Fixes 375 | 376 | * **deps:** update dependency @netlify/config to ^6.4.0 ([031477f](https://www.github.com/netlify/netlify-redirect-parser/commit/031477f270055c79ef5b1f000915636ff3eae008)) 377 | 378 | ### [3.0.20](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.19...v3.0.20) (2021-04-29) 379 | 380 | 381 | ### Bug Fixes 382 | 383 | * **deps:** update dependency @netlify/config to ^6.3.2 ([#126](https://www.github.com/netlify/netlify-redirect-parser/issues/126)) ([a7b3744](https://www.github.com/netlify/netlify-redirect-parser/commit/a7b37449cdc6495bf06ef656167be3ea5ada3227)) 384 | 385 | ### [3.0.19](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.18...v3.0.19) (2021-04-27) 386 | 387 | 388 | ### Bug Fixes 389 | 390 | * **deps:** update dependency @netlify/config to ^6.3.0 ([4f2f95a](https://www.github.com/netlify/netlify-redirect-parser/commit/4f2f95a5e06d59b8f0861e46c14fae9de64e4ab8)) 391 | 392 | ### [3.0.18](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.17...v3.0.18) (2021-04-26) 393 | 394 | 395 | ### Bug Fixes 396 | 397 | * **deps:** update netlify packages ([13213eb](https://www.github.com/netlify/netlify-redirect-parser/commit/13213eb63864d7d4e2112bebd8b9fc71efea57fc)) 398 | 399 | ### [3.0.17](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.16...v3.0.17) (2021-04-23) 400 | 401 | 402 | ### Bug Fixes 403 | 404 | * **deps:** update dependency @netlify/config to ^6.2.0 ([7bd6072](https://www.github.com/netlify/netlify-redirect-parser/commit/7bd60728892c76a48d0a4daa31b7eeae413bebf6)) 405 | 406 | ### [3.0.16](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.15...v3.0.16) (2021-04-20) 407 | 408 | 409 | ### Bug Fixes 410 | 411 | * **deps:** update dependency @netlify/config to ^6.0.2 ([#116](https://www.github.com/netlify/netlify-redirect-parser/issues/116)) ([ab3fcb9](https://www.github.com/netlify/netlify-redirect-parser/commit/ab3fcb9d5c5941bf05be01737947fd7f335dde10)) 412 | 413 | ### [3.0.15](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.14...v3.0.15) (2021-04-14) 414 | 415 | 416 | ### Bug Fixes 417 | 418 | * **deps:** update dependency @netlify/config to ^6.0.1 ([9dc48de](https://www.github.com/netlify/netlify-redirect-parser/commit/9dc48de65a357e5e1d25edf72a3630ca178201de)) 419 | * **deps:** update dependency @netlify/config to v6 ([4164f4b](https://www.github.com/netlify/netlify-redirect-parser/commit/4164f4ba0e0cecf6696260f6cdb27c87fc381756)) 420 | 421 | ### [3.0.14](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.13...v3.0.14) (2021-04-12) 422 | 423 | 424 | ### Bug Fixes 425 | 426 | * **deps:** update dependency @netlify/config to ^5.12.0 ([4ac7eb9](https://www.github.com/netlify/netlify-redirect-parser/commit/4ac7eb90e7bc3d86c6808369e5747d56a95cdb64)) 427 | * **deps:** update netlify packages ([#107](https://www.github.com/netlify/netlify-redirect-parser/issues/107)) ([0520be9](https://www.github.com/netlify/netlify-redirect-parser/commit/0520be934f676961e11f1b812c0104b12e31b214)) 428 | 429 | ### [3.0.13](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.12...v3.0.13) (2021-04-12) 430 | 431 | 432 | ### Bug Fixes 433 | 434 | * **deps:** update dependency @netlify/config to ^5.11.0 ([fc9ae7f](https://www.github.com/netlify/netlify-redirect-parser/commit/fc9ae7ff8cec43c30ab7c85eaefa4ee37d8b9985)) 435 | * **deps:** update dependency @netlify/config to ^5.9.0 ([abc0e58](https://www.github.com/netlify/netlify-redirect-parser/commit/abc0e58850f33091ffbd4c187baa92d99255bc83)) 436 | 437 | ### [3.0.12](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.11...v3.0.12) (2021-04-08) 438 | 439 | 440 | ### Bug Fixes 441 | 442 | * **deps:** update dependency @netlify/config to ^5.6.0 ([30c5d82](https://www.github.com/netlify/netlify-redirect-parser/commit/30c5d820ec924c3aaa2cd0c2babcc5226505dde4)) 443 | 444 | ### [3.0.11](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.10...v3.0.11) (2021-04-07) 445 | 446 | 447 | ### Bug Fixes 448 | 449 | * **deps:** update dependency @netlify/config to ^5.5.1 ([db1f132](https://www.github.com/netlify/netlify-redirect-parser/commit/db1f132b77ecf332c2ee215a85c80d8b5c231d49)) 450 | 451 | ### [3.0.10](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.9...v3.0.10) (2021-04-06) 452 | 453 | 454 | ### Bug Fixes 455 | 456 | * **deps:** update dependency @netlify/config to ^5.1.1 ([128bb84](https://www.github.com/netlify/netlify-redirect-parser/commit/128bb84cd86d8c353d29261603a33a53fb15bdcc)) 457 | 458 | ### [3.0.9](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.8...v3.0.9) (2021-03-31) 459 | 460 | 461 | ### Bug Fixes 462 | 463 | * **deps:** update dependency @netlify/config to v5 ([#83](https://www.github.com/netlify/netlify-redirect-parser/issues/83)) ([e00c74a](https://www.github.com/netlify/netlify-redirect-parser/commit/e00c74a1418a86919c456a78237b4ade74f628ba)) 464 | 465 | ### [3.0.8](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.7...v3.0.8) (2021-03-29) 466 | 467 | 468 | ### Bug Fixes 469 | 470 | * **deps:** update dependency @netlify/config to ^4.3.0 ([#78](https://www.github.com/netlify/netlify-redirect-parser/issues/78)) ([6ff82e5](https://www.github.com/netlify/netlify-redirect-parser/commit/6ff82e54ecff56b90211af921215045203e0561b)) 471 | 472 | ### [3.0.7](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.6...v3.0.7) (2021-03-10) 473 | 474 | 475 | ### Bug Fixes 476 | 477 | * **deps:** update dependency @netlify/config to ^4.1.2 ([8a88cff](https://www.github.com/netlify/netlify-redirect-parser/commit/8a88cff43bef11868dda3cc40dc67f5463d2040c)) 478 | 479 | ### [3.0.6](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.5...v3.0.6) (2021-03-09) 480 | 481 | 482 | ### Bug Fixes 483 | 484 | * **deps:** update dependency @netlify/config to ^4.1.1 ([920084e](https://www.github.com/netlify/netlify-redirect-parser/commit/920084e9e4f25e3f3f485326bfed685bbf84626a)) 485 | 486 | ### [3.0.5](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.4...v3.0.5) (2021-03-09) 487 | 488 | 489 | ### Bug Fixes 490 | 491 | * **deps:** update dependency @netlify/config to ^4.1.0 ([35c7d45](https://www.github.com/netlify/netlify-redirect-parser/commit/35c7d45b0bef04bcd2077687d002b5e5bfaccdc7)) 492 | 493 | ### [3.0.4](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.3...v3.0.4) (2021-03-04) 494 | 495 | 496 | ### Bug Fixes 497 | 498 | * **deps:** update dependency @netlify/config to ^4.0.4 ([#64](https://www.github.com/netlify/netlify-redirect-parser/issues/64)) ([b5f961d](https://www.github.com/netlify/netlify-redirect-parser/commit/b5f961dfd8da32e7111bbf2e54c90a48474dd5a0)) 499 | 500 | ### [3.0.3](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.2...v3.0.3) (2021-02-04) 501 | 502 | 503 | ### Bug Fixes 504 | 505 | * **deps:** update dependency @netlify/config to v4 ([#48](https://www.github.com/netlify/netlify-redirect-parser/issues/48)) ([7d6d1f8](https://www.github.com/netlify/netlify-redirect-parser/commit/7d6d1f81a27c9995c3d88470de5eaaa331dc1e4a)) 506 | 507 | ### [3.0.2](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.1...v3.0.2) (2021-01-22) 508 | 509 | 510 | ### Bug Fixes 511 | 512 | * **deps:** update dependency @netlify/config to v3.0.2 ([#38](https://www.github.com/netlify/netlify-redirect-parser/issues/38)) ([0bf95e9](https://www.github.com/netlify/netlify-redirect-parser/commit/0bf95e9fe3e5742f74d4e13c7c3a517e56d614fd)) 513 | 514 | ### [3.0.1](https://www.github.com/netlify/netlify-redirect-parser/compare/v3.0.0...v3.0.1) (2021-01-14) 515 | 516 | 517 | ### Bug Fixes 518 | 519 | * **deps:** update dependency @netlify/config to v3 ([#34](https://www.github.com/netlify/netlify-redirect-parser/issues/34)) ([31d0284](https://www.github.com/netlify/netlify-redirect-parser/commit/31d0284daa679d6161e8217f395844c837855cb3)) 520 | * **deps:** update dependency @netlify/config to v3.0.1 ([#36](https://www.github.com/netlify/netlify-redirect-parser/issues/36)) ([d53e713](https://www.github.com/netlify/netlify-redirect-parser/commit/d53e71313106a8e21989fe543c39c9face046846)) 521 | 522 | ## [3.0.0](https://www.github.com/netlify/netlify-redirect-parser/compare/v2.5.0...v3.0.0) (2021-01-11) 523 | 524 | 525 | ### ⚠ BREAKING CHANGES 526 | 527 | * add engines field (#24) 528 | 529 | ### Bug Fixes 530 | 531 | * add engines field ([#24](https://www.github.com/netlify/netlify-redirect-parser/issues/24)) ([4507a2a](https://www.github.com/netlify/netlify-redirect-parser/commit/4507a2af8154ed9f8ece781c8db465aeb56262ab)) 532 | * **deps:** update dependency @netlify/config to v2 ([#30](https://www.github.com/netlify/netlify-redirect-parser/issues/30)) ([71c52e8](https://www.github.com/netlify/netlify-redirect-parser/commit/71c52e819097938a65932e85fe6add961d3732e2)) 533 | 534 | ## [v2.5.0](https://github.com/netlify/netlify-redirect-parser/compare/v2.4.2...v2.5.0) - 2020-05-06 535 | 536 | ### Merged 537 | 538 | - Upgrade @netlify/config [`#15`](https://github.com/netlify/netlify-redirect-parser/pull/15) 539 | - Use lodash.isPlainObject [`#14`](https://github.com/netlify/netlify-redirect-parser/pull/14) 540 | 541 | ## [v2.4.2](https://github.com/netlify/netlify-redirect-parser/compare/v2.4.1...v2.4.2) - 2020-04-30 542 | 543 | ## [v2.4.1](https://github.com/netlify/netlify-redirect-parser/compare/v2.4.0...v2.4.1) - 2020-04-28 544 | 545 | ### Commits 546 | 547 | - Merge pull request #13 from netlify/feat/upgrade-netlify-config [`8f122e4`](https://github.com/netlify/netlify-redirect-parser/commit/8f122e436ee36cb647b1dd140f7e750a35e5b51b) 548 | - Upgrade `@netlify/config` [`13fde73`](https://github.com/netlify/netlify-redirect-parser/commit/13fde73883f9aea81313dd0ce100dad8eb4e59f4) 549 | 550 | ## [v2.4.0](https://github.com/netlify/netlify-redirect-parser/compare/v2.3.0...v2.4.0) - 2020-04-09 551 | 552 | ### Merged 553 | 554 | - Bump lodash.merge from 4.6.1 to 4.6.2 [`#4`](https://github.com/netlify/netlify-redirect-parser/pull/4) 555 | - Bump js-yaml from 3.12.2 to 3.13.1 [`#5`](https://github.com/netlify/netlify-redirect-parser/pull/5) 556 | - Upgrade `@netlify/config` [`#12`](https://github.com/netlify/netlify-redirect-parser/pull/12) 557 | 558 | ### Commits 559 | 560 | - Fix vulnerabilities [`3c754a1`](https://github.com/netlify/netlify-redirect-parser/commit/3c754a14ffabcc53d66b8a93cc4714fb4814d814) 561 | - Upgrade @netlify/config [`207e351`](https://github.com/netlify/netlify-redirect-parser/commit/207e351072712ea808512c6b1fa74779f4264780) 562 | 563 | ## [v2.3.0](https://github.com/netlify/netlify-redirect-parser/compare/v2.2.0...v2.3.0) - 2020-03-11 564 | 565 | ### Merged 566 | 567 | - Fix tests and add new [`#10`](https://github.com/netlify/netlify-redirect-parser/pull/10) 568 | - Upgrade `@netlify/config` [`#9`](https://github.com/netlify/netlify-redirect-parser/pull/9) 569 | 570 | ### Commits 571 | 572 | - Fix tests [`814468a`](https://github.com/netlify/netlify-redirect-parser/commit/814468af8ed7127e49b27ee8d055259438b96a07) 573 | - Upgrade to latest @netlify/config [`adaadbb`](https://github.com/netlify/netlify-redirect-parser/commit/adaadbba4d8a21642e2d064fc7f1408612193bb2) 574 | - Add test for netlify.toml [`335a0f6`](https://github.com/netlify/netlify-redirect-parser/commit/335a0f631b3cbee5e9fb1cedf89c5334028f362d) 575 | 576 | ## [v2.2.0](https://github.com/netlify/netlify-redirect-parser/compare/v2.0.3...v2.2.0) - 2020-01-07 577 | 578 | ### Commits 579 | 580 | - Add new files to release [`68663a5`](https://github.com/netlify/netlify-redirect-parser/commit/68663a53f3645392258df097cdbe044be48f174e) 581 | 582 | ## [v2.0.3](https://github.com/netlify/netlify-redirect-parser/compare/v2.0.2...v2.0.3) - 2020-01-07 583 | 584 | ### Commits 585 | 586 | - Formatting [`717cbcb`](https://github.com/netlify/netlify-redirect-parser/commit/717cbcb76434bbf1394241650ae07c779d23109b) 587 | 588 | ## [v2.0.2](https://github.com/netlify/netlify-redirect-parser/compare/v2.0.1...v2.0.2) - 2020-01-05 589 | 590 | ### Commits 591 | 592 | - Update package-lock.json [`abef4f6`](https://github.com/netlify/netlify-redirect-parser/commit/abef4f6c0ac29573753137a5821aa471ccfa9065) 593 | 594 | ## [v2.0.1](https://github.com/netlify/netlify-redirect-parser/compare/v2.0.0...v2.0.1) - 2019-12-20 595 | 596 | ### Commits 597 | 598 | - Fix repo paths [`aff6a45`](https://github.com/netlify/netlify-redirect-parser/commit/aff6a45d3fd02eb5c8e4b5bb70e9f26b183888f5) 599 | 600 | ## [v2.0.0](https://github.com/netlify/netlify-redirect-parser/compare/v1.0.2...v2.0.0) - 2019-12-20 601 | 602 | ### Merged 603 | 604 | - Revamp parsing by using @netlify/config [`#8`](https://github.com/netlify/netlify-redirect-parser/pull/8) 605 | - Add support for YAML config [`#7`](https://github.com/netlify/netlify-redirect-parser/pull/7) 606 | - Add move to CLI notice [`#2`](https://github.com/netlify/netlify-redirect-parser/pull/2) 607 | 608 | ### Commits 609 | 610 | - Revamp config parsing [`4d16720`](https://github.com/netlify/netlify-redirect-parser/commit/4d16720249d556e22c1357eb6c1f57379f4d7da0) 611 | - Clean dependencies [`244050c`](https://github.com/netlify/netlify-redirect-parser/commit/244050c8d3b026189f7d8fb7adb066145d90dc30) 612 | - Add YAML parser [`97ff75b`](https://github.com/netlify/netlify-redirect-parser/commit/97ff75b614398d465b637b1c8658a37df5514262) 613 | 614 | ## v1.0.2 - 2019-05-10 615 | 616 | ### Commits 617 | 618 | - First commit [`5e94b8a`](https://github.com/netlify/netlify-redirect-parser/commit/5e94b8abf7b811f1b7f0b71bcbba3a7b76674bdd) 619 | - prettify and add changelogs [`e9a5417`](https://github.com/netlify/netlify-redirect-parser/commit/e9a5417d70886d684816bf4cc51bbe607f45c7c1) 620 | - Parse toml files as well [`9ee43cf`](https://github.com/netlify/netlify-redirect-parser/commit/9ee43cf2d88feff28af56dde38a73ab23324fff6) 621 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making 6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, 7 | disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, 8 | religion, or sexual identity and orientation. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment include: 13 | 14 | - Using welcoming and inclusive language 15 | - Being respectful of differing viewpoints and experiences 16 | - Gracefully accepting constructive criticism 17 | - Focusing on what is best for the community 18 | - Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 23 | - Trolling, insulting/derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 26 | - Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Our Responsibilities 29 | 30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take 31 | appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any 35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the 40 | project or its community. Examples of representing a project or community include using an official project e-mail 41 | address, posting via an official social media account, or acting as an appointed representative at an online or offline 42 | event. Representation of a project may be further defined and clarified by project maintainers. 43 | 44 | ## Enforcement 45 | 46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at 47 | david@netlify.com. All complaints will be reviewed and investigated and will result in a response that is deemed 48 | necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to 49 | the reporter of an incident. Further details of specific enforcement policies may be posted separately. 50 | 51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent 52 | repercussions as determined by other members of the project's leadership. 53 | 54 | ## Attribution 55 | 56 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at 57 | [http://contributor-covenant.org/version/1/4][version] 58 | 59 | [homepage]: http://contributor-covenant.org 60 | [version]: http://contributor-covenant.org/version/1/4/ 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | Contributions are always welcome, no matter how large or small. Before contributing, please read the 4 | [code of conduct](CODE_OF_CONDUCT.md). 5 | 6 | ## Setup 7 | 8 | > Install [Node.js](https://nodejs.org/en/download/) on your system 9 | 10 | ```sh 11 | git clone git@github.com:netlify/netlify-redirect-parser.git 12 | cd netlify-redirect-parser 13 | npm install 14 | npm test 15 | ``` 16 | 17 | ## Testing 18 | 19 | The following things are tested for: 20 | 21 | - Formatting 22 | - Linting 23 | - Functionality via Unit Tests 24 | 25 | ## Releasing 26 | 27 | Merge the release PR 28 | 29 | ## License 30 | 31 | By contributing to Netlify Redirects Parser, you agree that your contributions will be licensed under its 32 | [MIT license](LICENSE). 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Netlify 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ## !important Notice 2 | > 3 | > This repository was moved into the mono repository of [github.com/netlify/build](https://github.com/netlify/build) The 4 | > package name and the versions are preserved! 5 | 6 | # Netlify Redirect Parser 7 | 8 | [![Coverage Status](https://codecov.io/gh/netlify/netlify-redirect-parser/branch/main/graph/badge.svg)](https://codecov.io/gh/netlify/netlify-redirect-parser) 9 | [![Tests](https://github.com/netlify/netlify-redirect-parser/workflows/Test/badge.svg)](https://github.com/netlify/netlify-redirect-parser/actions) 10 | 11 | Parses redirect rules from both `_redirects` and `netlify.toml` and normalizes them to an array of objects. 12 | 13 | For most users, you are not meant to use this directly, please refer to https://github.com/netlify/cli instead. However 14 | if you are debugging issues with redirect parsing, issues and PRs are welcome. 15 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | strict_yaml_branch: main 3 | coverage: 4 | range: [80, 100] 5 | parsers: 6 | javascript: 7 | enable_partials: true 8 | status: 9 | project: false 10 | patch: false 11 | comment: false 12 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netlify-redirect-parser", 3 | "version": "13.0.5", 4 | "description": "Parses netlify redirects into a js object representation", 5 | "type": "module", 6 | "exports": "./src/index.js", 7 | "main": "./src/index.js", 8 | "scripts": { 9 | "prepare": "husky install node_modules/@netlify/eslint-config-node/.husky/", 10 | "prepublishOnly": "npm ci && npm test", 11 | "test": "run-s format test:dev", 12 | "test:dev": "ava", 13 | "test:ci": "c8 -r lcovonly -r text -r json ava", 14 | "format": "run-s format:check-fix:*", 15 | "format:ci": "run-s format:check:*", 16 | "format:check-fix:lint": "run-e format:check:lint format:fix:lint", 17 | "format:check:lint": "cross-env-shell eslint $npm_package_config_eslint", 18 | "format:fix:lint": "cross-env-shell eslint --fix $npm_package_config_eslint", 19 | "format:check-fix:prettier": "run-e format:check:prettier format:fix:prettier", 20 | "format:check:prettier": "cross-env-shell prettier --check $npm_package_config_prettier", 21 | "format:fix:prettier": "cross-env-shell prettier --write $npm_package_config_prettier" 22 | }, 23 | "config": { 24 | "eslint": "--ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"*.{cjs,mjs,js,md}\" \"{src,tests}/**/*.{cjs,mjs,js}\"", 25 | "prettier": "--ignore-path .gitignore --loglevel=warn \".github/**/*.{md,yml}\" \"*.{cjs,mjs,js,yml,json}\" \"{src,tests}/**/*.{cjs,mjs,js}\" \"!package-lock.json\" \"!CHANGELOG.md\"" 26 | }, 27 | "keywords": [ 28 | "netlify" 29 | ], 30 | "engines": { 31 | "node": "^12.20.0 || ^14.14.0 || >=16.0.0" 32 | }, 33 | "author": "Netlify", 34 | "license": "MIT", 35 | "dependencies": { 36 | "filter-obj": "^3.0.0", 37 | "is-plain-obj": "^4.0.0", 38 | "path-exists": "^5.0.0", 39 | "toml": "^3.0.0" 40 | }, 41 | "devDependencies": { 42 | "@netlify/eslint-config-node": "^6.0.0", 43 | "ava": "^4.0.0", 44 | "c8": "^7.11.0", 45 | "husky": "^7.0.4", 46 | "test-each": "^4.0.0" 47 | }, 48 | "files": [ 49 | "src/**/*.js", 50 | "!src/**/*.test.js" 51 | ], 52 | "ava": { 53 | "files": [ 54 | "tests/*.js" 55 | ], 56 | "verbose": true 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "git+https://github.com/netlify/netlify-redirect-parser.git" 61 | }, 62 | "bugs": { 63 | "url": "https://github.com/netlify/netlify-redirect-parser/issues" 64 | }, 65 | "homepage": "https://github.com/netlify/netlify-redirect-parser#readme" 66 | } 67 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: ['github>netlify/renovate-config:esm'], 3 | ignorePresets: [':prHourlyLimit2'], 4 | semanticCommits: true, 5 | dependencyDashboard: true, 6 | packageRules: [], 7 | } 8 | -------------------------------------------------------------------------------- /src/all.js: -------------------------------------------------------------------------------- 1 | import { parseFileRedirects } from './line_parser.js' 2 | import { mergeRedirects } from './merge.js' 3 | import { parseConfigRedirects } from './netlify_config_parser.js' 4 | import { normalizeRedirects } from './normalize.js' 5 | import { splitResults, concatResults } from './results.js' 6 | 7 | // Parse all redirects given programmatically via the `configRedirects` property, `netlify.toml` and `_redirects` files, then normalize 8 | // and validate those. 9 | export const parseAllRedirects = async function ({ 10 | redirectsFiles = [], 11 | netlifyConfigPath, 12 | configRedirects = [], 13 | minimal = false, 14 | }) { 15 | const [ 16 | { redirects: fileRedirects, errors: fileParseErrors }, 17 | { redirects: parsedConfigRedirects, errors: configParseErrors }, 18 | ] = await Promise.all([getFileRedirects(redirectsFiles), getConfigRedirects(netlifyConfigPath)]) 19 | const { redirects: normalizedFileRedirects, errors: fileNormalizeErrors } = normalizeRedirects(fileRedirects, minimal) 20 | const { redirects: normalizedParsedConfigRedirects, errors: parsedConfigNormalizeErrors } = normalizeRedirects( 21 | parsedConfigRedirects, 22 | minimal, 23 | ) 24 | const { redirects: normalizedConfigRedirects, errors: configNormalizeErrors } = normalizeRedirects( 25 | configRedirects, 26 | minimal, 27 | ) 28 | const { redirects, errors: mergeErrors } = mergeRedirects({ 29 | fileRedirects: normalizedFileRedirects, 30 | configRedirects: [...normalizedParsedConfigRedirects, ...normalizedConfigRedirects], 31 | }) 32 | const errors = [ 33 | ...fileParseErrors, 34 | ...fileNormalizeErrors, 35 | ...configParseErrors, 36 | ...parsedConfigNormalizeErrors, 37 | ...configNormalizeErrors, 38 | ...mergeErrors, 39 | ] 40 | return { redirects, errors } 41 | } 42 | 43 | const getFileRedirects = async function (redirectsFiles) { 44 | const resultsArrays = await Promise.all(redirectsFiles.map((redirectFile) => parseFileRedirects(redirectFile))) 45 | return concatResults(resultsArrays) 46 | } 47 | 48 | const getConfigRedirects = async function (netlifyConfigPath) { 49 | if (netlifyConfigPath === undefined) { 50 | return splitResults([]) 51 | } 52 | 53 | return await parseConfigRedirects(netlifyConfigPath) 54 | } 55 | -------------------------------------------------------------------------------- /src/conditions.js: -------------------------------------------------------------------------------- 1 | // Normalize conditions 2 | export const normalizeConditions = function (rawConditions) { 3 | const caseNormalizedConditions = normalizeConditionCases(rawConditions) 4 | const listNormalizedConditions = normalizeConditionLists(caseNormalizedConditions) 5 | return listNormalizedConditions 6 | } 7 | 8 | // Conditions can optionally be capitalized 9 | const normalizeConditionCases = function (conditions) { 10 | return CONDITION_CAPITALIZED_PROPS.reduce(normalizeConditionCase, conditions) 11 | } 12 | 13 | const CONDITION_CAPITALIZED_PROPS = [ 14 | { name: 'role', capitalizedName: 'Role' }, 15 | { name: 'language', capitalizedName: 'Language' }, 16 | { name: 'country', capitalizedName: 'Country' }, 17 | ] 18 | 19 | const normalizeConditionCase = function (conditions, { name, capitalizedName }) { 20 | const { [capitalizedName]: capitalizedProp, [name]: prop = capitalizedProp, ...conditionsA } = conditions 21 | return prop === undefined ? conditionsA : { ...conditionsA, [capitalizedName]: prop } 22 | } 23 | 24 | // Some `conditions` are array of strings. 25 | // In `_redirects`, they are comma-separated lists instead. 26 | const normalizeConditionLists = function (conditions) { 27 | return CONDITION_LIST_PROPS.reduce(normalizeConditionList, conditions) 28 | } 29 | 30 | const CONDITION_LIST_PROPS = ['Role', 'Language', 'Country'] 31 | 32 | const normalizeConditionList = function (conditions, name) { 33 | return typeof conditions[name] === 'string' 34 | ? { ...conditions, [name]: conditions[name].trim().split(CONDITION_LIST_REGEXP) } 35 | : conditions 36 | } 37 | 38 | const CONDITION_LIST_REGEXP = /\s*,\s*/gu 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { parseAllRedirects } from './all.js' 2 | -------------------------------------------------------------------------------- /src/line_parser.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | 3 | import { pathExists } from 'path-exists' 4 | 5 | import { splitResults } from './results.js' 6 | import { transtypeStatusCode, isValidStatusCode } from './status.js' 7 | import { isUrl } from './url.js' 8 | 9 | // Parse `_redirects` file to an array of objects. 10 | // Each line in that file must be either: 11 | // - An empty line 12 | // - A comment starting with # 13 | // - A redirect line, optionally ended with a comment 14 | // Each redirect line has the following format: 15 | // from [query] [to] [status[!]] [conditions] 16 | // The parts are: 17 | // - "from": a path or a URL 18 | // - "query": a whitespace-separated list of "key=value" 19 | // - "to": a path or a URL 20 | // - "status": an HTTP status integer 21 | // - "!": an optional exclamation mark appended to "status" meant to indicate 22 | // "forced" 23 | // - "conditions": a whitespace-separated list of "key=value" 24 | // - "Sign" is a special condition 25 | // Unlike "redirects" in "netlify.toml", the "headers" and "edge_handlers" 26 | // cannot be specified. 27 | export const parseFileRedirects = async function (redirectFile) { 28 | const results = await parseRedirects(redirectFile) 29 | return splitResults(results) 30 | } 31 | 32 | const parseRedirects = async function (redirectFile) { 33 | if (!(await pathExists(redirectFile))) { 34 | return [] 35 | } 36 | 37 | const text = await readRedirectFile(redirectFile) 38 | if (typeof text !== 'string') { 39 | return [text] 40 | } 41 | return text 42 | .split('\n') 43 | .map(normalizeLine) 44 | .filter(hasRedirect) 45 | .map((redirectLine) => parseRedirect(redirectLine)) 46 | } 47 | 48 | const readRedirectFile = async function (redirectFile) { 49 | try { 50 | return await fs.readFile(redirectFile, 'utf8') 51 | } catch { 52 | return new Error(`Could not read redirects file: ${redirectFile}`) 53 | } 54 | } 55 | 56 | const normalizeLine = function (line, index) { 57 | return { line: line.trim(), index } 58 | } 59 | 60 | const hasRedirect = function ({ line }) { 61 | return line !== '' && !isComment(line) 62 | } 63 | 64 | const parseRedirect = function ({ line, index }) { 65 | try { 66 | return parseRedirectLine(line) 67 | } catch (error) { 68 | return new Error(`Could not parse redirect line ${index + 1}: 69 | ${line} 70 | ${error.message}`) 71 | } 72 | } 73 | 74 | // Parse a single redirect line 75 | const parseRedirectLine = function (line) { 76 | const [from, ...parts] = trimComment(line.split(LINE_TOKENS_REGEXP)) 77 | 78 | if (parts.length === 0) { 79 | throw new Error('Missing destination path/URL') 80 | } 81 | 82 | const { 83 | queryParts, 84 | to, 85 | lastParts: [statusPart, ...conditionsParts], 86 | } = parseParts(from, parts) 87 | 88 | const query = parsePairs(queryParts) 89 | const { status, force } = parseStatus(statusPart) 90 | const { Sign, signed = Sign, ...conditions } = parsePairs(conditionsParts) 91 | return { from, query, to, status, force, conditions, signed } 92 | } 93 | 94 | // Removes inline comments at the end of the line 95 | const trimComment = function (parts) { 96 | const commentIndex = parts.findIndex(isComment) 97 | return commentIndex === -1 ? parts : parts.slice(0, commentIndex) 98 | } 99 | 100 | const isComment = function (part) { 101 | return part.startsWith('#') 102 | } 103 | 104 | const LINE_TOKENS_REGEXP = /\s+/g 105 | 106 | // Figure out the purpose of each whitelist-separated part, taking into account 107 | // the fact that some are optional. 108 | const parseParts = function (from, parts) { 109 | // Optional `to` field when using a forward rule. 110 | // The `to` field is added and validated later on, so we can leave it 111 | // `undefined` 112 | if (isValidStatusCode(transtypeStatusCode(parts[0]))) { 113 | return { queryParts: [], to: undefined, lastParts: parts } 114 | } 115 | 116 | const toIndex = parts.findIndex(isToPart) 117 | if (toIndex === -1) { 118 | throw new Error('The destination path/URL must start with "/", "http:" or "https:"') 119 | } 120 | 121 | const queryParts = parts.slice(0, toIndex) 122 | const to = parts[toIndex] 123 | const lastParts = parts.slice(toIndex + 1) 124 | return { queryParts, to, lastParts } 125 | } 126 | 127 | const isToPart = function (part) { 128 | return part.startsWith('/') || isUrl(part) 129 | } 130 | 131 | // Parse the `status` part 132 | const parseStatus = function (statusPart) { 133 | if (statusPart === undefined) { 134 | return {} 135 | } 136 | 137 | const status = transtypeStatusCode(statusPart) 138 | 139 | if (!isValidStatusCode(status)) { 140 | return { status: statusPart, force: false } 141 | } 142 | 143 | const force = statusPart.endsWith('!') 144 | return { status, force } 145 | } 146 | 147 | // Part key=value pairs used for both the `query` and `conditions` parts 148 | const parsePairs = function (conditions) { 149 | return Object.assign({}, ...conditions.map(parsePair)) 150 | } 151 | 152 | const parsePair = function (condition) { 153 | const [key, value] = condition.split('=') 154 | return { [key]: value } 155 | } 156 | -------------------------------------------------------------------------------- /src/merge.js: -------------------------------------------------------------------------------- 1 | import { isDeepStrictEqual } from 'util' 2 | 3 | import { splitResults } from './results.js' 4 | 5 | // Merge redirects from `_redirects` with the ones from `netlify.toml`. 6 | // When both are specified, both are used and `_redirects` has priority. 7 | // Since in both `netlify.toml` and `_redirects`, only the first matching rule 8 | // is used, it is possible to merge `_redirects` to `netlify.toml` by prepending 9 | // its rules to `netlify.toml` `redirects` field. 10 | export const mergeRedirects = function ({ fileRedirects, configRedirects }) { 11 | const results = [...fileRedirects, ...configRedirects] 12 | const { redirects, errors } = splitResults(results) 13 | const mergedRedirects = redirects.filter(isUniqueRedirect) 14 | return { redirects: mergedRedirects, errors } 15 | } 16 | 17 | // Remove duplicates. This is especially likely considering `fileRedirects` 18 | // might have been previously merged to `configRedirects`, which happens when 19 | // `netlifyConfig.redirects` is modified by plugins. 20 | // The latest duplicate value is the one kept. 21 | const isUniqueRedirect = function (redirect, index, redirects) { 22 | return !redirects.slice(index + 1).some((otherRedirect) => isDeepStrictEqual(redirect, otherRedirect)) 23 | } 24 | -------------------------------------------------------------------------------- /src/netlify_config_parser.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | 3 | import { pathExists } from 'path-exists' 4 | import { parse as loadToml } from 'toml' 5 | 6 | import { splitResults } from './results.js' 7 | 8 | // Parse `redirects` field in "netlify.toml" to an array of objects. 9 | // This field is already an array of objects so it only validates and 10 | // normalizes it. 11 | export const parseConfigRedirects = async function (netlifyConfigPath) { 12 | if (!(await pathExists(netlifyConfigPath))) { 13 | return splitResults([]) 14 | } 15 | 16 | const results = await parseConfig(netlifyConfigPath) 17 | return splitResults(results) 18 | } 19 | 20 | // Load the configuration file and parse it (TOML) 21 | const parseConfig = async function (configPath) { 22 | try { 23 | const configString = await fs.readFile(configPath, 'utf8') 24 | const config = loadToml(configString) 25 | // Convert `null` prototype objects to normal plain objects 26 | const { redirects = [] } = JSON.parse(JSON.stringify(config)) 27 | if (!Array.isArray(redirects)) { 28 | throw new TypeError(`"redirects" must be an array`) 29 | } 30 | return redirects 31 | } catch (error) { 32 | return [new Error(`Could not parse configuration file: ${error}`)] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/normalize.js: -------------------------------------------------------------------------------- 1 | import filterObj from 'filter-obj' 2 | import isPlainObj from 'is-plain-obj' 3 | 4 | import { normalizeConditions } from './conditions.js' 5 | import { splitResults } from './results.js' 6 | import { normalizeStatus } from './status.js' 7 | import { isUrl } from './url.js' 8 | 9 | // Validate and normalize an array of `redirects` objects. 10 | // This step is performed after `redirects` have been parsed from either 11 | // `netlify.toml` or `_redirects`. 12 | export const normalizeRedirects = function (redirects, minimal) { 13 | if (!Array.isArray(redirects)) { 14 | const error = new TypeError(`Redirects must be an array not: ${redirects}`) 15 | return splitResults([error]) 16 | } 17 | 18 | const results = redirects.map((obj, index) => parseRedirect(obj, index, minimal)) 19 | return splitResults(results) 20 | } 21 | 22 | const parseRedirect = function (obj, index, minimal) { 23 | if (!isPlainObj(obj)) { 24 | return new TypeError(`Redirects must be objects not: ${obj}`) 25 | } 26 | 27 | try { 28 | return parseRedirectObject(obj, minimal) 29 | } catch (error) { 30 | return new Error(`Could not parse redirect number ${index + 1}: 31 | ${JSON.stringify(obj)} 32 | ${error.message}`) 33 | } 34 | } 35 | 36 | // Parse a single `redirects` object 37 | const parseRedirectObject = function ( 38 | { 39 | // `from` used to be named `origin` 40 | origin, 41 | from = origin, 42 | // `query` used to be named `params` and `parameters` 43 | parameters = {}, 44 | params = parameters, 45 | query = params, 46 | // `to` used to be named `destination` 47 | destination, 48 | to = destination, 49 | status, 50 | force = false, 51 | conditions = {}, 52 | // `signed` used to be named `signing` and `sign` 53 | sign, 54 | signing = sign, 55 | signed = signing, 56 | headers = {}, 57 | }, 58 | minimal, 59 | ) { 60 | if (from === undefined) { 61 | throw new Error('Missing "from" field') 62 | } 63 | 64 | if (!isPlainObj(headers)) { 65 | throw new Error('"headers" field must be an object') 66 | } 67 | 68 | const statusA = normalizeStatus(status) 69 | const finalTo = addForwardRule(from, statusA, to) 70 | const { scheme, host, path } = parseFrom(from) 71 | const proxy = isProxy(statusA, finalTo) 72 | const normalizedConditions = normalizeConditions(conditions) 73 | 74 | // We ensure the return value has the same shape as our `netlify-commons` 75 | // backend 76 | return removeUndefinedValues({ 77 | from, 78 | query, 79 | to: finalTo, 80 | status: statusA, 81 | force, 82 | conditions: normalizedConditions, 83 | signed, 84 | headers, 85 | // If `minimal: true`, does not add additional properties that are not 86 | // valid in `netlify.toml` 87 | ...(!minimal && { scheme, host, path, proxy }), 88 | }) 89 | } 90 | 91 | // Add the optional `to` field when using a forward rule 92 | const addForwardRule = function (from, status, to) { 93 | if (to !== undefined) { 94 | return to 95 | } 96 | 97 | if (!isSplatRule(from, status)) { 98 | throw new Error('Missing "to" field') 99 | } 100 | 101 | return from.replace(SPLAT_REGEXP, '/:splat') 102 | } 103 | 104 | // "to" can only be omitted when using forward rules: 105 | // - This requires "from" to end with "/*" and "status" to be 2** 106 | // - "to" will then default to "from" but with "/*" replaced to "/:splat" 107 | const isSplatRule = function (from, status) { 108 | return from.endsWith('/*') && status >= 200 && status < 300 109 | } 110 | 111 | const SPLAT_REGEXP = /\/\*$/ 112 | 113 | // Parses the `from` field which can be either a file path or a URL. 114 | const parseFrom = function (from) { 115 | const { scheme, host, path } = parseFromField(from) 116 | if (path.startsWith('/.netlify')) { 117 | throw new Error('"path" field must not start with "/.netlify"') 118 | } 119 | 120 | return { scheme, host, path } 121 | } 122 | 123 | const parseFromField = function (from) { 124 | if (!isUrl(from)) { 125 | return { path: from } 126 | } 127 | 128 | try { 129 | const { host, protocol, pathname: path } = new URL(from) 130 | const scheme = protocol.slice(0, -1) 131 | return { scheme, host, path } 132 | } catch (error) { 133 | throw new Error(`Invalid URL: ${error.message}`) 134 | } 135 | } 136 | 137 | const isProxy = function (status, to) { 138 | return status === 200 && isUrl(to) 139 | } 140 | 141 | const removeUndefinedValues = function (object) { 142 | return filterObj(object, isDefined) 143 | } 144 | 145 | const isDefined = function (key, value) { 146 | return value !== undefined 147 | } 148 | -------------------------------------------------------------------------------- /src/results.js: -------------------------------------------------------------------------------- 1 | // If one redirect fails to parse, we still try to return the other ones 2 | export const splitResults = function (results) { 3 | const redirects = results.filter((result) => !isError(result)) 4 | const errors = results.filter(isError) 5 | return { redirects, errors } 6 | } 7 | 8 | const isError = function (result) { 9 | return result instanceof Error 10 | } 11 | 12 | // Concatenate an array of `{ redirects, erors }` 13 | export const concatResults = function (resultsArrays) { 14 | const redirects = resultsArrays.flatMap(getRedirects) 15 | const errors = resultsArrays.flatMap(getErrors) 16 | return { redirects, errors } 17 | } 18 | 19 | const getRedirects = function ({ redirects }) { 20 | return redirects 21 | } 22 | 23 | const getErrors = function ({ errors }) { 24 | return errors 25 | } 26 | -------------------------------------------------------------------------------- /src/status.js: -------------------------------------------------------------------------------- 1 | // Normalize `status` field 2 | export const normalizeStatus = function (status) { 3 | if (status === undefined) { 4 | return 5 | } 6 | 7 | const statusCode = transtypeStatusCode(status) 8 | if (!isValidStatusCode(statusCode)) { 9 | throw new Error(`Invalid status code: ${status}`) 10 | } 11 | return statusCode 12 | } 13 | 14 | // Transtype `status` string to a number. 15 | // `status` might be a string ending with `!`. If so, `Number.parseInt()` strips 16 | // and ignores it. 17 | export const transtypeStatusCode = function (status) { 18 | return Number.parseInt(status) 19 | } 20 | 21 | // Check whether the field is a valid status code 22 | export const isValidStatusCode = function (status) { 23 | return Number.isInteger(status) 24 | } 25 | -------------------------------------------------------------------------------- /src/url.js: -------------------------------------------------------------------------------- 1 | // Check if a field is valid redirect URL 2 | export const isUrl = function (pathOrUrl) { 3 | return SCHEMES.some((scheme) => pathOrUrl.startsWith(scheme)) 4 | } 5 | 6 | const SCHEMES = ['http://', 'https://'] 7 | -------------------------------------------------------------------------------- /tests/all.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { each } from 'test-each' 3 | 4 | import { validateSuccess, validateErrors } from './helpers/main.js' 5 | 6 | each( 7 | [ 8 | { 9 | title: 'empty', 10 | input: {}, 11 | output: [], 12 | }, 13 | { 14 | title: 'only_config', 15 | input: { 16 | netlifyConfigPath: 'from_simple', 17 | }, 18 | output: [ 19 | { 20 | from: '/old-path', 21 | path: '/old-path', 22 | to: '/new-path', 23 | }, 24 | ], 25 | }, 26 | { 27 | title: 'only_files', 28 | input: { 29 | redirectsFiles: ['from_simple', 'from_absolute_uri'], 30 | }, 31 | output: [ 32 | { 33 | from: '/home', 34 | path: '/home', 35 | to: '/', 36 | }, 37 | { 38 | from: 'http://hello.bitballoon.com/*', 39 | scheme: 'http', 40 | host: 'hello.bitballoon.com', 41 | path: '/*', 42 | to: 'http://www.hello.com/:splat', 43 | }, 44 | ], 45 | }, 46 | { 47 | title: 'both_config_files', 48 | input: { 49 | redirectsFiles: ['from_simple', 'from_absolute_uri'], 50 | netlifyConfigPath: 'from_simple', 51 | }, 52 | output: [ 53 | { 54 | from: '/home', 55 | path: '/home', 56 | to: '/', 57 | }, 58 | { 59 | from: 'http://hello.bitballoon.com/*', 60 | scheme: 'http', 61 | host: 'hello.bitballoon.com', 62 | path: '/*', 63 | to: 'http://www.hello.com/:splat', 64 | }, 65 | { 66 | from: '/old-path', 67 | path: '/old-path', 68 | to: '/new-path', 69 | }, 70 | ], 71 | }, 72 | { 73 | title: 'config_redirects', 74 | input: { 75 | netlifyConfigPath: 'from_simple', 76 | configRedirects: [ 77 | { 78 | from: '/home', 79 | to: '/', 80 | }, 81 | ], 82 | }, 83 | output: [ 84 | { 85 | from: '/old-path', 86 | path: '/old-path', 87 | to: '/new-path', 88 | }, 89 | { 90 | from: '/home', 91 | path: '/home', 92 | to: '/', 93 | }, 94 | ], 95 | }, 96 | { 97 | title: 'minimal', 98 | input: { 99 | redirectsFiles: ['from_simple', 'from_absolute_uri'], 100 | netlifyConfigPath: 'from_simple', 101 | minimal: true, 102 | }, 103 | output: [ 104 | { 105 | from: '/home', 106 | to: '/', 107 | }, 108 | { 109 | from: 'http://hello.bitballoon.com/*', 110 | to: 'http://www.hello.com/:splat', 111 | }, 112 | { 113 | from: '/old-path', 114 | to: '/new-path', 115 | }, 116 | ], 117 | }, 118 | { 119 | title: 'valid_redirects_mixed', 120 | input: { 121 | redirectsFiles: ['from_simple'], 122 | configRedirects: {}, 123 | }, 124 | output: [ 125 | { 126 | from: '/home', 127 | path: '/home', 128 | to: '/', 129 | }, 130 | ], 131 | }, 132 | ], 133 | ({ title }, opts) => { 134 | test(`Parses netlify.toml and _redirects | ${title}`, async (t) => { 135 | await validateSuccess(t, opts) 136 | }) 137 | }, 138 | ) 139 | 140 | each( 141 | [ 142 | { 143 | title: 'invalid_redirects_array', 144 | input: { 145 | configRedirects: {}, 146 | }, 147 | errorMessage: /must be an array/, 148 | }, 149 | { 150 | title: 'invalid_redirects_mixed', 151 | input: { 152 | redirectsFiles: ['from_simple'], 153 | configRedirects: {}, 154 | }, 155 | errorMessage: /must be an array/, 156 | }, 157 | ], 158 | ({ title }, opts) => { 159 | test(`Validate syntax errors | ${title}`, async (t) => { 160 | await validateErrors(t, opts) 161 | }) 162 | }, 163 | ) 164 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/backward_compat_destination.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | destination = "/new-path" 4 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/backward_compat_origin.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | origin = "/old-path" 3 | to = "/new-path" 4 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/backward_compat_parameters.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | [redirects.parameters] 5 | path = ":path" 6 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/backward_compat_params.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | [redirects.params] 5 | path = ":path" 6 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/backward_compat_sign.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | sign = "api_key" 5 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/backward_compat_signing.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | signing = "api_key" 5 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/complex.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | status = 301 5 | force = false 6 | query = {path = ":path"} 7 | conditions = {Language = ["en"], Country = ["US"], Role = ["admin"]} 8 | 9 | ## This rule redirects to an external API, signing requests with a secret 10 | [[redirects]] 11 | from = "/search" 12 | to = "https://api.mysearch.com" 13 | status = 200 14 | force = true # COMMENT: ensure that we always redirect 15 | headers = {X-From = "Netlify"} 16 | signed = "API_SIGNATURE_TOKEN" 17 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/conditions_country_case.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | conditions = {Country = ["US"]} 5 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/conditions_language_case.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | conditions = {Language = ["en"]} 5 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/conditions_role_case.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | conditions = {Role = ["admin"]} 5 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/empty.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/netlify-redirect-parser/ff29d776b53fdb4bf9760ae8060145d8870bf9f3/tests/fixtures/netlify_config/empty.toml -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/from_forward.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path/*" 3 | status = 200 4 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/from_no_slash.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "old-path" 3 | to = "new-path" 4 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/from_simple.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/from_url.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "http://www.example.com/old-path" 3 | to = "http://www.example.com/new-path" 4 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_dot_netlify_path.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/.netlify/from" 3 | to = "/new-path" 4 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_dot_netlify_url.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "http://www.example.com/.netlify/from" 3 | to = "http://www.example.com/new-path" 4 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_forward_status.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path/*" 3 | status = 300 4 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_headers.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | [[redirects.headers]] 5 | example = "value" 6 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_no_from.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | to = "/new-path" 3 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_no_to.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_object.toml: -------------------------------------------------------------------------------- 1 | redirects = ["/old-path"] 2 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_status_empty.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "new-path" 4 | status = "" 5 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_status_high.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "new-path" 4 | status = 2000 5 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_status_negative.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "new-path" 4 | status = -200 5 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_status_string.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "new-path" 4 | status = "not_a_status" 5 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_toml.toml: -------------------------------------------------------------------------------- 1 | {{}} 2 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_type.toml: -------------------------------------------------------------------------------- 1 | [redirects] 2 | from = "/old-path" 3 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/invalid_url.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "http://" 3 | to = "http://www.example.com/new-path" 4 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/minimal.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/here" 3 | to = "/there" 4 | status = 200 5 | force = true 6 | headers = {X-From = "Netlify"} 7 | query = {path = ":path"} 8 | conditions = {Language = ["en"], Country = ["US"], Role = ["admin"]} 9 | signed = "API_SIGNATURE_TOKEN" 10 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/query.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | [redirects.query] 5 | path = ":path" 6 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/signed.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | signed = "api_key" 5 | -------------------------------------------------------------------------------- /tests/fixtures/netlify_config/status_string.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/old-path" 3 | to = "/new-path" 4 | status = "200" 5 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/comment_full: -------------------------------------------------------------------------------- 1 | # this is just an old leftover 2 | /blog/my-post.php /blog/my-post 3 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/comment_inline: -------------------------------------------------------------------------------- 1 | /blog/my-post.php /blog/my-post # this is just an old leftover 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/conditions_country: -------------------------------------------------------------------------------- 1 | / /china 302 Country=ch,tw 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/conditions_country_case: -------------------------------------------------------------------------------- 1 | /old-path /new-path 200 Country=US 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/conditions_country_language: -------------------------------------------------------------------------------- 1 | / /china 302 Country=il Language=en 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/conditions_language_case: -------------------------------------------------------------------------------- 1 | /old-path /new-path 200 Language=en 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/conditions_query: -------------------------------------------------------------------------------- 1 | /donate source=:source email=:email /donate/usa?source=:source&email=:email 302 Country=us 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/conditions_role: -------------------------------------------------------------------------------- 1 | /admin/* /admin/:splat 200 Role=admin 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/conditions_role_case: -------------------------------------------------------------------------------- 1 | /old-path /new-path 200 Role=admin 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/conditions_roles: -------------------------------------------------------------------------------- 1 | /member/* /member/:splat 200 Role=admin,member 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/netlify-redirect-parser/ff29d776b53fdb4bf9760ae8060145d8870bf9f3/tests/fixtures/redirects_file/empty -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/empty_line: -------------------------------------------------------------------------------- 1 | /blog/my-post.php /blog/my-post 2 | 3 | /blog/my-post-two.php /blog/my-post-two 4 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/from_absolute_uri: -------------------------------------------------------------------------------- 1 | http://hello.bitballoon.com/* http://www.hello.com/:splat 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/from_simple: -------------------------------------------------------------------------------- 1 | /home / 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/invalid_dir/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/netlify-redirect-parser/ff29d776b53fdb4bf9760ae8060145d8870bf9f3/tests/fixtures/redirects_file/invalid_dir/.gitkeep -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/invalid_dot_netlify_path: -------------------------------------------------------------------------------- 1 | /.netlify/from /to 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/invalid_dot_netlify_url: -------------------------------------------------------------------------------- 1 | http://example.com/.netlify/from /to 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/invalid_mistaken_headers: -------------------------------------------------------------------------------- 1 | # Protect backups with Basic Auth 2 | /backup/* 3 | Basic-Auth: dev@terraeclipse.com:nud-feud-contour 4 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/invalid_no_slash: -------------------------------------------------------------------------------- 1 | / index.html 2 | /blog blog.html 3 | /trt-therapy-san-diego trt-therapy-san-diego.html 4 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/invalid_no_to_no_status: -------------------------------------------------------------------------------- 1 | /m/scge/legal 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/invalid_no_to_query: -------------------------------------------------------------------------------- 1 | /m/scge/legal source=:source 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/invalid_no_to_status: -------------------------------------------------------------------------------- 1 | /swfobject.html?detectflash=false 301 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/invalid_status: -------------------------------------------------------------------------------- 1 | /from /to not_a_status 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/invalid_url: -------------------------------------------------------------------------------- 1 | http:// /to 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/line_trim: -------------------------------------------------------------------------------- 1 | /home / 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/multiple_lines: -------------------------------------------------------------------------------- 1 | /10thmagnitude http://www.10thmagnitude.com/ 301 2 | /bananastand http://eepurl.com/Lgde5 301 3 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/proxy: -------------------------------------------------------------------------------- 1 | /api/* https://api.bitballoon.com/* 200 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/query: -------------------------------------------------------------------------------- 1 | / page=news /news 2 | /blog post=:post_id /blog/:post_id 3 | / _escaped_fragment_=/about /about 4 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/signed: -------------------------------------------------------------------------------- 1 | /api/* https://api.example.com/:splat 200! Sign=API_SECRET 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/signed_backward_compat: -------------------------------------------------------------------------------- 1 | /api/* https://api.example.com/:splat 200! signed=API_SECRET 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/simple: -------------------------------------------------------------------------------- 1 | /one /two 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/simple_multiple: -------------------------------------------------------------------------------- 1 | /one /two 2 | /one /four 3 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/status: -------------------------------------------------------------------------------- 1 | /test https://www.bitballoon.com/test=hello 301 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/status_force: -------------------------------------------------------------------------------- 1 | /test https://www.bitballoon.com/test=hello 301! 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/to_anchor: -------------------------------------------------------------------------------- 1 | /blog/my-post-ads.php /blog/my-post#ads # this is a valid anchor with a comment 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/to_path_forward: -------------------------------------------------------------------------------- 1 | /admin/* 200 2 | /admin/* 200! 3 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/to_splat_force: -------------------------------------------------------------------------------- 1 | /* https://www.bitballoon.com/:splat 301! 2 | -------------------------------------------------------------------------------- /tests/fixtures/redirects_file/to_splat_no_force: -------------------------------------------------------------------------------- 1 | /* https://www.bitballoon.com/:splat 301 2 | -------------------------------------------------------------------------------- /tests/helpers/main.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url' 2 | 3 | import { parseAllRedirects } from '../../src/index.js' 4 | 5 | const FIXTURES_DIR = fileURLToPath(new URL('../fixtures', import.meta.url)) 6 | 7 | // Pass an `input` to the main method and assert its output 8 | export const validateSuccess = async function (t, { input, output }) { 9 | const { redirects } = await parseRedirects(input) 10 | t.deepEqual( 11 | redirects, 12 | output.map((redirect) => normalizeRedirect(redirect, input)), 13 | ) 14 | } 15 | 16 | // Pass an `input` to the main method and assert it fails with a specific error 17 | export const validateErrors = async function (t, { input, errorMessage }) { 18 | const { errors } = await parseRedirects(input) 19 | t.not(errors.length, 0) 20 | t.true(errors.some((error) => errorMessage.test(error.message))) 21 | } 22 | 23 | const parseRedirects = async function ({ redirectsFiles, netlifyConfigPath, configRedirects, minimal }) { 24 | return await parseAllRedirects({ 25 | ...(redirectsFiles && { redirectsFiles: redirectsFiles.map(addFileFixtureDir) }), 26 | ...(netlifyConfigPath && { netlifyConfigPath: addConfigFixtureDir(netlifyConfigPath) }), 27 | configRedirects, 28 | minimal, 29 | }) 30 | } 31 | 32 | const addFileFixtureDir = function (name) { 33 | return `${FIXTURES_DIR}/redirects_file/${name}` 34 | } 35 | 36 | const addConfigFixtureDir = function (name) { 37 | return `${FIXTURES_DIR}/netlify_config/${name}.toml` 38 | } 39 | 40 | // Assign default values to redirects 41 | const normalizeRedirect = function (redirect, { minimal }) { 42 | return { 43 | ...(minimal || ADDED_DEFAULT_REDIRECTS), 44 | ...DEFAULT_REDIRECT, 45 | ...redirect, 46 | } 47 | } 48 | 49 | const ADDED_DEFAULT_REDIRECTS = { 50 | proxy: false, 51 | } 52 | 53 | const DEFAULT_REDIRECT = { 54 | force: false, 55 | query: {}, 56 | conditions: {}, 57 | headers: {}, 58 | } 59 | -------------------------------------------------------------------------------- /tests/line_parser.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { each } from 'test-each' 3 | 4 | import { validateSuccess, validateErrors } from './helpers/main.js' 5 | 6 | each( 7 | [ 8 | { 9 | title: 'empty', 10 | input: { 11 | redirectsFiles: ['empty'], 12 | }, 13 | output: [], 14 | }, 15 | { 16 | title: 'non_existing', 17 | input: { 18 | redirectsFiles: ['non_existing'], 19 | }, 20 | output: [], 21 | }, 22 | { 23 | title: 'empty_line', 24 | input: { 25 | redirectsFiles: ['empty_line'], 26 | }, 27 | output: [ 28 | { 29 | from: '/blog/my-post.php', 30 | path: '/blog/my-post.php', 31 | to: '/blog/my-post', 32 | }, 33 | { 34 | from: '/blog/my-post-two.php', 35 | path: '/blog/my-post-two.php', 36 | to: '/blog/my-post-two', 37 | }, 38 | ], 39 | }, 40 | { 41 | title: 'multiple_lines', 42 | input: { 43 | redirectsFiles: ['multiple_lines'], 44 | }, 45 | output: [ 46 | { 47 | from: '/10thmagnitude', 48 | path: '/10thmagnitude', 49 | to: 'http://www.10thmagnitude.com/', 50 | status: 301, 51 | }, 52 | { 53 | from: '/bananastand', 54 | path: '/bananastand', 55 | to: 'http://eepurl.com/Lgde5', 56 | status: 301, 57 | }, 58 | ], 59 | }, 60 | { 61 | title: 'line_trim', 62 | input: { 63 | redirectsFiles: ['line_trim'], 64 | }, 65 | output: [ 66 | { 67 | from: '/home', 68 | path: '/home', 69 | to: '/', 70 | }, 71 | ], 72 | }, 73 | { 74 | title: 'comment_full', 75 | input: { 76 | redirectsFiles: ['comment_full'], 77 | }, 78 | output: [ 79 | { 80 | from: '/blog/my-post.php', 81 | path: '/blog/my-post.php', 82 | to: '/blog/my-post', 83 | }, 84 | ], 85 | }, 86 | { 87 | title: 'comment_inline', 88 | input: { 89 | redirectsFiles: ['comment_inline'], 90 | }, 91 | output: [ 92 | { 93 | from: '/blog/my-post.php', 94 | path: '/blog/my-post.php', 95 | to: '/blog/my-post', 96 | }, 97 | ], 98 | }, 99 | { 100 | title: 'from_simple', 101 | input: { 102 | redirectsFiles: ['from_simple'], 103 | }, 104 | output: [ 105 | { 106 | from: '/home', 107 | path: '/home', 108 | to: '/', 109 | }, 110 | ], 111 | }, 112 | { 113 | title: 'from_absolute_uri', 114 | input: { 115 | redirectsFiles: ['from_absolute_uri'], 116 | }, 117 | output: [ 118 | { 119 | from: 'http://hello.bitballoon.com/*', 120 | scheme: 'http', 121 | host: 'hello.bitballoon.com', 122 | path: '/*', 123 | to: 'http://www.hello.com/:splat', 124 | }, 125 | ], 126 | }, 127 | { 128 | title: 'query', 129 | input: { 130 | redirectsFiles: ['query'], 131 | }, 132 | output: [ 133 | { 134 | from: '/', 135 | path: '/', 136 | to: '/news', 137 | query: { page: 'news' }, 138 | }, 139 | { 140 | from: '/blog', 141 | path: '/blog', 142 | to: '/blog/:post_id', 143 | query: { post: ':post_id' }, 144 | }, 145 | { 146 | from: '/', 147 | path: '/', 148 | to: '/about', 149 | query: { _escaped_fragment_: '/about' }, 150 | }, 151 | ], 152 | }, 153 | { 154 | title: 'to_anchor', 155 | input: { 156 | redirectsFiles: ['to_anchor'], 157 | }, 158 | output: [ 159 | { 160 | from: '/blog/my-post-ads.php', 161 | path: '/blog/my-post-ads.php', 162 | to: '/blog/my-post#ads', 163 | }, 164 | ], 165 | }, 166 | { 167 | title: 'to_splat_no_force', 168 | input: { 169 | redirectsFiles: ['to_splat_no_force'], 170 | }, 171 | output: [ 172 | { 173 | from: '/*', 174 | path: '/*', 175 | to: 'https://www.bitballoon.com/:splat', 176 | status: 301, 177 | }, 178 | ], 179 | }, 180 | { 181 | title: 'to_splat_force', 182 | input: { 183 | redirectsFiles: ['to_splat_force'], 184 | }, 185 | output: [ 186 | { 187 | from: '/*', 188 | path: '/*', 189 | to: 'https://www.bitballoon.com/:splat', 190 | status: 301, 191 | force: true, 192 | }, 193 | ], 194 | }, 195 | { 196 | title: 'to_path_forward', 197 | input: { 198 | redirectsFiles: ['to_path_forward'], 199 | }, 200 | output: [ 201 | { 202 | from: '/admin/*', 203 | path: '/admin/*', 204 | to: '/admin/:splat', 205 | status: 200, 206 | }, 207 | { 208 | from: '/admin/*', 209 | path: '/admin/*', 210 | to: '/admin/:splat', 211 | status: 200, 212 | force: true, 213 | }, 214 | ], 215 | }, 216 | { 217 | title: 'proxy', 218 | input: { 219 | redirectsFiles: ['proxy'], 220 | }, 221 | output: [ 222 | { 223 | from: '/api/*', 224 | path: '/api/*', 225 | to: 'https://api.bitballoon.com/*', 226 | status: 200, 227 | proxy: true, 228 | }, 229 | ], 230 | }, 231 | { 232 | title: 'status', 233 | input: { 234 | redirectsFiles: ['status'], 235 | }, 236 | output: [ 237 | { 238 | from: '/test', 239 | path: '/test', 240 | to: 'https://www.bitballoon.com/test=hello', 241 | status: 301, 242 | }, 243 | ], 244 | }, 245 | { 246 | title: 'status_force', 247 | input: { 248 | redirectsFiles: ['status_force'], 249 | }, 250 | output: [ 251 | { 252 | from: '/test', 253 | path: '/test', 254 | to: 'https://www.bitballoon.com/test=hello', 255 | status: 301, 256 | force: true, 257 | }, 258 | ], 259 | }, 260 | { 261 | title: 'conditions_country', 262 | input: { 263 | redirectsFiles: ['conditions_country'], 264 | }, 265 | output: [ 266 | { 267 | from: '/', 268 | path: '/', 269 | to: '/china', 270 | status: 302, 271 | conditions: { Country: ['ch', 'tw'] }, 272 | }, 273 | ], 274 | }, 275 | { 276 | title: 'conditions_country_language', 277 | input: { 278 | redirectsFiles: ['conditions_country_language'], 279 | }, 280 | output: [ 281 | { 282 | from: '/', 283 | path: '/', 284 | to: '/china', 285 | status: 302, 286 | conditions: { Country: ['il'], Language: ['en'] }, 287 | }, 288 | ], 289 | }, 290 | { 291 | title: 'conditions_role', 292 | input: { 293 | redirectsFiles: ['conditions_role'], 294 | }, 295 | output: [ 296 | { 297 | from: '/admin/*', 298 | path: '/admin/*', 299 | to: '/admin/:splat', 300 | status: 200, 301 | conditions: { Role: ['admin'] }, 302 | }, 303 | ], 304 | }, 305 | { 306 | title: 'conditions_roles', 307 | input: { 308 | redirectsFiles: ['conditions_roles'], 309 | }, 310 | output: [ 311 | { 312 | from: '/member/*', 313 | path: '/member/*', 314 | to: '/member/:splat', 315 | status: 200, 316 | conditions: { Role: ['admin', 'member'] }, 317 | }, 318 | ], 319 | }, 320 | { 321 | title: 'conditions_query', 322 | input: { 323 | redirectsFiles: ['conditions_query'], 324 | }, 325 | output: [ 326 | { 327 | from: '/donate', 328 | path: '/donate', 329 | to: '/donate/usa?source=:source&email=:email', 330 | status: 302, 331 | query: { source: ':source', email: ':email' }, 332 | conditions: { Country: ['us'] }, 333 | }, 334 | ], 335 | }, 336 | { 337 | title: 'conditions_country_case', 338 | input: { 339 | redirectsFiles: ['conditions_country_case'], 340 | }, 341 | output: [ 342 | { 343 | from: '/old-path', 344 | path: '/old-path', 345 | to: '/new-path', 346 | status: 200, 347 | conditions: { Country: ['US'] }, 348 | }, 349 | ], 350 | }, 351 | { 352 | title: 'conditions_language_case', 353 | input: { 354 | redirectsFiles: ['conditions_language_case'], 355 | }, 356 | output: [ 357 | { 358 | from: '/old-path', 359 | path: '/old-path', 360 | to: '/new-path', 361 | status: 200, 362 | conditions: { Language: ['en'] }, 363 | }, 364 | ], 365 | }, 366 | { 367 | title: 'conditions_role_case', 368 | input: { 369 | redirectsFiles: ['conditions_role_case'], 370 | }, 371 | output: [ 372 | { 373 | from: '/old-path', 374 | path: '/old-path', 375 | to: '/new-path', 376 | status: 200, 377 | conditions: { Role: ['admin'] }, 378 | }, 379 | ], 380 | }, 381 | { 382 | title: 'signed', 383 | input: { 384 | redirectsFiles: ['signed'], 385 | }, 386 | output: [ 387 | { 388 | from: '/api/*', 389 | path: '/api/*', 390 | to: 'https://api.example.com/:splat', 391 | status: 200, 392 | proxy: true, 393 | force: true, 394 | signed: 'API_SECRET', 395 | }, 396 | ], 397 | }, 398 | { 399 | title: 'signed_backward_compat', 400 | input: { 401 | redirectsFiles: ['signed_backward_compat'], 402 | }, 403 | output: [ 404 | { 405 | from: '/api/*', 406 | path: '/api/*', 407 | to: 'https://api.example.com/:splat', 408 | status: 200, 409 | proxy: true, 410 | force: true, 411 | signed: 'API_SECRET', 412 | }, 413 | ], 414 | }, 415 | ], 416 | ({ title }, opts) => { 417 | test(`Parses _redirects | ${title}`, async (t) => { 418 | await validateSuccess(t, opts) 419 | }) 420 | }, 421 | ) 422 | 423 | each( 424 | [ 425 | { 426 | title: 'invalid_dir', 427 | input: { 428 | redirectsFiles: ['invalid_dir'], 429 | }, 430 | errorMessage: /read redirects file/, 431 | }, 432 | { 433 | title: 'invalid_url', 434 | input: { 435 | redirectsFiles: ['invalid_url'], 436 | }, 437 | errorMessage: /Invalid URL/, 438 | }, 439 | { 440 | title: 'invalid_dot_netlify_url', 441 | input: { 442 | redirectsFiles: ['invalid_dot_netlify_url'], 443 | }, 444 | errorMessage: /must not start/, 445 | }, 446 | { 447 | title: 'invalid_dot_netlify_path', 448 | input: { 449 | redirectsFiles: ['invalid_dot_netlify_path'], 450 | }, 451 | errorMessage: /must not start/, 452 | }, 453 | { 454 | title: 'invalid_status', 455 | input: { 456 | redirectsFiles: ['invalid_status'], 457 | }, 458 | errorMessage: /Invalid status code/, 459 | }, 460 | { 461 | title: 'invalid_no_to_no_status', 462 | input: { 463 | redirectsFiles: ['invalid_no_to_no_status'], 464 | }, 465 | errorMessage: /Missing destination/, 466 | }, 467 | { 468 | title: 'invalid_no_to_status', 469 | input: { 470 | redirectsFiles: ['invalid_no_to_status'], 471 | }, 472 | errorMessage: /Missing "to" field/, 473 | }, 474 | { 475 | title: 'invalid_no_to_query', 476 | input: { 477 | redirectsFiles: ['invalid_no_to_query'], 478 | }, 479 | errorMessage: /must start with/, 480 | }, 481 | { 482 | title: 'invalid_no_slash', 483 | input: { 484 | redirectsFiles: ['invalid_no_slash'], 485 | }, 486 | errorMessage: /must start with/, 487 | }, 488 | { 489 | title: 'invalid_mistaken_headers', 490 | input: { 491 | redirectsFiles: ['invalid_mistaken_headers'], 492 | }, 493 | errorMessage: /Missing destination/, 494 | }, 495 | ], 496 | ({ title }, opts) => { 497 | test(`Validate syntax errors | ${title}`, async (t) => { 498 | await validateErrors(t, opts) 499 | }) 500 | }, 501 | ) 502 | -------------------------------------------------------------------------------- /tests/merge.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { each } from 'test-each' 3 | 4 | import { validateSuccess } from './helpers/main.js' 5 | 6 | each( 7 | [ 8 | { 9 | title: 'undefined', 10 | input: {}, 11 | output: [], 12 | }, 13 | { 14 | title: 'empty', 15 | input: { 16 | redirectsFiles: ['empty'], 17 | }, 18 | output: [], 19 | }, 20 | { 21 | title: 'simple', 22 | input: { 23 | redirectsFiles: ['simple'], 24 | }, 25 | output: [ 26 | { 27 | from: '/one', 28 | path: '/one', 29 | to: '/two', 30 | }, 31 | ], 32 | }, 33 | { 34 | title: 'only_config', 35 | input: { 36 | redirectsFiles: ['empty'], 37 | configRedirects: [ 38 | { 39 | from: '/one', 40 | to: '/three', 41 | }, 42 | ], 43 | }, 44 | output: [ 45 | { 46 | from: '/one', 47 | path: '/one', 48 | to: '/three', 49 | }, 50 | ], 51 | }, 52 | { 53 | title: 'simple_merge', 54 | input: { 55 | redirectsFiles: ['simple'], 56 | configRedirects: [ 57 | { 58 | from: '/one', 59 | to: '/three', 60 | }, 61 | ], 62 | }, 63 | output: [ 64 | { 65 | from: '/one', 66 | path: '/one', 67 | to: '/two', 68 | }, 69 | { 70 | from: '/one', 71 | path: '/one', 72 | to: '/three', 73 | }, 74 | ], 75 | }, 76 | { 77 | title: 'simple_multiple', 78 | input: { 79 | redirectsFiles: ['simple_multiple'], 80 | configRedirects: [ 81 | { 82 | from: '/one', 83 | to: '/two', 84 | }, 85 | { 86 | from: '/one', 87 | to: '/three', 88 | }, 89 | ], 90 | }, 91 | output: [ 92 | { 93 | from: '/one', 94 | path: '/one', 95 | to: '/four', 96 | }, 97 | { 98 | from: '/one', 99 | path: '/one', 100 | to: '/two', 101 | }, 102 | { 103 | from: '/one', 104 | path: '/one', 105 | to: '/three', 106 | }, 107 | ], 108 | }, 109 | ], 110 | ({ title }, opts) => { 111 | test(`Merges _redirects with netlify.toml redirects | ${title}`, async (t) => { 112 | await validateSuccess(t, opts) 113 | }) 114 | }, 115 | ) 116 | -------------------------------------------------------------------------------- /tests/netlify_config_parser.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { each } from 'test-each' 3 | 4 | import { validateSuccess, validateErrors } from './helpers/main.js' 5 | 6 | each( 7 | [ 8 | { 9 | title: 'empty', 10 | input: { 11 | netlifyConfigPath: 'empty', 12 | }, 13 | output: [], 14 | }, 15 | { 16 | title: 'non_existing', 17 | input: { 18 | netlifyConfigPath: 'non_existing', 19 | }, 20 | output: [], 21 | }, 22 | { 23 | title: 'backward_compat_origin', 24 | input: { 25 | netlifyConfigPath: 'backward_compat_origin', 26 | }, 27 | output: [ 28 | { 29 | from: '/old-path', 30 | path: '/old-path', 31 | to: '/new-path', 32 | }, 33 | ], 34 | }, 35 | { 36 | title: 'backward_compat_destination', 37 | input: { 38 | netlifyConfigPath: 'backward_compat_destination', 39 | }, 40 | output: [ 41 | { 42 | from: '/old-path', 43 | path: '/old-path', 44 | to: '/new-path', 45 | }, 46 | ], 47 | }, 48 | { 49 | title: 'backward_compat_params', 50 | input: { 51 | netlifyConfigPath: 'backward_compat_params', 52 | }, 53 | output: [ 54 | { 55 | from: '/old-path', 56 | path: '/old-path', 57 | to: '/new-path', 58 | query: { path: ':path' }, 59 | }, 60 | ], 61 | }, 62 | { 63 | title: 'backward_compat_parameters', 64 | input: { 65 | netlifyConfigPath: 'backward_compat_parameters', 66 | }, 67 | output: [ 68 | { 69 | from: '/old-path', 70 | path: '/old-path', 71 | to: '/new-path', 72 | query: { path: ':path' }, 73 | }, 74 | ], 75 | }, 76 | { 77 | title: 'backward_compat_sign', 78 | input: { 79 | netlifyConfigPath: 'backward_compat_sign', 80 | }, 81 | output: [ 82 | { 83 | from: '/old-path', 84 | path: '/old-path', 85 | to: '/new-path', 86 | signed: 'api_key', 87 | }, 88 | ], 89 | }, 90 | { 91 | title: 'backward_compat_signing', 92 | input: { 93 | netlifyConfigPath: 'backward_compat_signing', 94 | }, 95 | output: [ 96 | { 97 | from: '/old-path', 98 | path: '/old-path', 99 | to: '/new-path', 100 | signed: 'api_key', 101 | }, 102 | ], 103 | }, 104 | { 105 | title: 'from_simple', 106 | input: { 107 | netlifyConfigPath: 'from_simple', 108 | }, 109 | output: [ 110 | { 111 | from: '/old-path', 112 | path: '/old-path', 113 | to: '/new-path', 114 | }, 115 | ], 116 | }, 117 | { 118 | title: 'from_url', 119 | input: { 120 | netlifyConfigPath: 'from_url', 121 | }, 122 | output: [ 123 | { 124 | from: 'http://www.example.com/old-path', 125 | scheme: 'http', 126 | host: 'www.example.com', 127 | path: '/old-path', 128 | to: 'http://www.example.com/new-path', 129 | }, 130 | ], 131 | }, 132 | { 133 | title: 'status_string', 134 | input: { 135 | netlifyConfigPath: 'status_string', 136 | }, 137 | output: [ 138 | { 139 | from: '/old-path', 140 | path: '/old-path', 141 | to: '/new-path', 142 | status: 200, 143 | }, 144 | ], 145 | }, 146 | { 147 | title: 'from_forward', 148 | input: { 149 | netlifyConfigPath: 'from_forward', 150 | }, 151 | output: [ 152 | { 153 | from: '/old-path/*', 154 | path: '/old-path/*', 155 | to: '/old-path/:splat', 156 | status: 200, 157 | }, 158 | ], 159 | }, 160 | { 161 | title: 'from_no_slash', 162 | input: { 163 | netlifyConfigPath: 'from_no_slash', 164 | }, 165 | output: [ 166 | { 167 | from: 'old-path', 168 | path: 'old-path', 169 | to: 'new-path', 170 | }, 171 | ], 172 | }, 173 | { 174 | title: 'query', 175 | input: { 176 | netlifyConfigPath: 'query', 177 | }, 178 | output: [ 179 | { 180 | from: '/old-path', 181 | path: '/old-path', 182 | to: '/new-path', 183 | query: { path: ':path' }, 184 | }, 185 | ], 186 | }, 187 | { 188 | title: 'conditions_country_case', 189 | input: { 190 | netlifyConfigPath: 'conditions_country_case', 191 | }, 192 | output: [ 193 | { 194 | from: '/old-path', 195 | path: '/old-path', 196 | to: '/new-path', 197 | conditions: { Country: ['US'] }, 198 | }, 199 | ], 200 | }, 201 | { 202 | title: 'conditions_language_case', 203 | input: { 204 | netlifyConfigPath: 'conditions_language_case', 205 | }, 206 | output: [ 207 | { 208 | from: '/old-path', 209 | path: '/old-path', 210 | to: '/new-path', 211 | conditions: { Language: ['en'] }, 212 | }, 213 | ], 214 | }, 215 | { 216 | title: 'conditions_role_case', 217 | input: { 218 | netlifyConfigPath: 'conditions_role_case', 219 | }, 220 | output: [ 221 | { 222 | from: '/old-path', 223 | path: '/old-path', 224 | to: '/new-path', 225 | conditions: { Role: ['admin'] }, 226 | }, 227 | ], 228 | }, 229 | { 230 | title: 'signed', 231 | input: { 232 | netlifyConfigPath: 'signed', 233 | }, 234 | output: [ 235 | { 236 | from: '/old-path', 237 | path: '/old-path', 238 | to: '/new-path', 239 | signed: 'api_key', 240 | }, 241 | ], 242 | }, 243 | { 244 | title: 'complex', 245 | input: { 246 | netlifyConfigPath: 'complex', 247 | }, 248 | output: [ 249 | { 250 | from: '/old-path', 251 | path: '/old-path', 252 | to: '/new-path', 253 | status: 301, 254 | query: { 255 | path: ':path', 256 | }, 257 | conditions: { 258 | Country: ['US'], 259 | Language: ['en'], 260 | Role: ['admin'], 261 | }, 262 | }, 263 | { 264 | from: '/search', 265 | path: '/search', 266 | to: 'https://api.mysearch.com', 267 | status: 200, 268 | proxy: true, 269 | force: true, 270 | signed: 'API_SIGNATURE_TOKEN', 271 | headers: { 272 | 'X-From': 'Netlify', 273 | }, 274 | }, 275 | ], 276 | }, 277 | { 278 | title: 'minimal', 279 | input: { 280 | netlifyConfigPath: 'minimal', 281 | minimal: true, 282 | }, 283 | output: [ 284 | { 285 | from: '/here', 286 | to: '/there', 287 | status: 200, 288 | force: true, 289 | signed: 'API_SIGNATURE_TOKEN', 290 | headers: { 291 | 'X-From': 'Netlify', 292 | }, 293 | query: { 294 | path: ':path', 295 | }, 296 | conditions: { 297 | Country: ['US'], 298 | Language: ['en'], 299 | Role: ['admin'], 300 | }, 301 | }, 302 | ], 303 | }, 304 | ], 305 | ({ title }, opts) => { 306 | test(`Parses netlify.toml redirects | ${title}`, async (t) => { 307 | await validateSuccess(t, opts) 308 | }) 309 | }, 310 | ) 311 | 312 | each( 313 | [ 314 | { 315 | title: 'invalid_toml', 316 | input: { 317 | netlifyConfigPath: 'invalid_toml', 318 | }, 319 | errorMessage: /parse configuration file/, 320 | }, 321 | { 322 | title: 'invalid_type', 323 | input: { 324 | netlifyConfigPath: 'invalid_type', 325 | }, 326 | errorMessage: /must be an array/, 327 | }, 328 | { 329 | title: 'invalid_object', 330 | input: { 331 | netlifyConfigPath: 'invalid_object', 332 | }, 333 | errorMessage: /must be objects/, 334 | }, 335 | { 336 | title: 'invalid_no_from', 337 | input: { 338 | netlifyConfigPath: 'invalid_no_from', 339 | }, 340 | errorMessage: /Missing "from"/, 341 | }, 342 | { 343 | title: 'invalid_no_to', 344 | input: { 345 | netlifyConfigPath: 'invalid_no_to', 346 | }, 347 | errorMessage: /Missing "to"/, 348 | }, 349 | { 350 | title: 'invalid_status_string', 351 | input: { 352 | netlifyConfigPath: 'invalid_status_string', 353 | }, 354 | errorMessage: /Invalid status code/, 355 | }, 356 | { 357 | title: 'invalid_status_empty', 358 | input: { 359 | netlifyConfigPath: 'invalid_status_empty', 360 | }, 361 | errorMessage: /Invalid status code/, 362 | }, 363 | { 364 | title: 'invalid_forward_status', 365 | input: { 366 | netlifyConfigPath: 'invalid_forward_status', 367 | }, 368 | errorMessage: /Missing "to"/, 369 | }, 370 | { 371 | title: 'invalid_url', 372 | input: { 373 | netlifyConfigPath: 'invalid_url', 374 | }, 375 | errorMessage: /Invalid URL/, 376 | }, 377 | { 378 | title: 'invalid_dot_netlify_url', 379 | input: { 380 | netlifyConfigPath: 'invalid_dot_netlify_url', 381 | }, 382 | errorMessage: /must not start/, 383 | }, 384 | { 385 | title: 'invalid_dot_netlify_path', 386 | input: { 387 | netlifyConfigPath: 'invalid_dot_netlify_path', 388 | }, 389 | errorMessage: /must not start/, 390 | }, 391 | { 392 | title: 'invalid_headers', 393 | input: { 394 | netlifyConfigPath: 'invalid_headers', 395 | }, 396 | errorMessage: /must be an object/, 397 | }, 398 | ], 399 | ({ title }, opts) => { 400 | test(`Validate syntax errors | ${title}`, async (t) => { 401 | await validateErrors(t, opts) 402 | }) 403 | }, 404 | ) 405 | --------------------------------------------------------------------------------