├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── lib ├── getHashDigest.js ├── hash │ ├── BatchedHash.js │ ├── BulkUpdateDecorator.js │ ├── md4.js │ ├── wasm-hash.js │ └── xxhash64.js ├── index.js ├── interpolateName.js ├── isUrlRequest.js └── urlToRequest.js ├── package.json ├── test ├── .eslintrc.json ├── getHashDigest.test.js ├── interpolateName.test.js ├── isUrlRequest.test.js └── urlToRequest.test.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [test/cases/parsing/bom/bomfile.{css,js}] 12 | charset = utf-8-bom 13 | 14 | [*.md] 15 | insert_final_newline = true 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "plugins": ["node"], 4 | "extends": ["eslint:recommended", "plugin:node/recommended"], 5 | "env": { 6 | "node": true 7 | }, 8 | "rules": { 9 | "no-template-curly-in-string": "error", 10 | "no-caller": "error", 11 | "yoda": "error", 12 | "eqeqeq": "error", 13 | "no-extra-bind": "error", 14 | "no-process-exit": "error", 15 | "no-loop-func": "error", 16 | "no-console": "off", 17 | "valid-jsdoc": "error", 18 | "no-var": "error", 19 | "prefer-const": "error", 20 | "prefer-arrow-callback": "error", 21 | "object-shorthand": "error" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: webpack 2 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: loader-utils 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - next 8 | pull_request: 9 | branches: 10 | - master 11 | - next 12 | 13 | permissions: 14 | contents: read # to fetch code (actions/checkout) 15 | 16 | jobs: 17 | lint: 18 | permissions: 19 | contents: read # to fetch code (actions/checkout) 20 | pull-requests: read # to get commits in PR (wagoid/commitlint-github-action) 21 | 22 | name: Lint - ${{ matrix.os }} - Node v${{ matrix.node-version }} 23 | 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | strategy: 28 | matrix: 29 | os: [ubuntu-latest] 30 | node-version: [12.x] 31 | 32 | runs-on: ${{ matrix.os }} 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Use Node.js ${{ matrix.node-version }} 40 | uses: actions/setup-node@v2 41 | with: 42 | node-version: ${{ matrix.node-version }} 43 | cache: "yarn" 44 | 45 | - name: Install dependencies 46 | run: yarn 47 | 48 | - name: Lint 49 | run: yarn lint 50 | 51 | - name: Security audit 52 | run: yarn audit --groups dependencies 53 | 54 | - name: Check commit message 55 | uses: wagoid/commitlint-github-action@v4 56 | 57 | test: 58 | name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }} 59 | 60 | strategy: 61 | matrix: 62 | os: [ubuntu-latest, windows-latest, macos-latest] 63 | node-version: [12.x, 14.x, 16.x, 17.x] 64 | 65 | runs-on: ${{ matrix.os }} 66 | 67 | steps: 68 | - name: Setup Git 69 | if: matrix.os == 'windows-latest' 70 | run: git config --global core.autocrlf input 71 | 72 | - uses: actions/checkout@v2 73 | - uses: actions/github-script@v7 74 | id: calculate_architecture 75 | with: 76 | result-encoding: string 77 | script: | 78 | if ('${{ matrix.os }}' === 'macos-latest' && ('${{ matrix['node-version'] }}' === '12.x' || '${{ matrix['node-version'] }}' === '14.x')) { 79 | return "x64" 80 | } else { 81 | return '' 82 | } 83 | - name: Use Node.js ${{ matrix.node-version }} 84 | uses: actions/setup-node@v2 85 | with: 86 | node-version: ${{ matrix.node-version }} 87 | architecture: ${{ steps.calculate_architecture.outputs.result }} 88 | cache: "yarn" 89 | 90 | - name: Install dependencies 91 | run: yarn 92 | 93 | - name: Run tests 94 | run: yarn test:only 95 | 96 | - name: Submit coverage data to codecov 97 | uses: codecov/codecov-action@v2 98 | with: 99 | token: ${{ secrets.CODECOV_TOKEN }} 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /test/fixtures 5 | CHANGELOG.md -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [3.3.1](https://github.com/webpack/loader-utils/compare/v3.3.0...v3.3.1) (2024-06-05) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * base64safe regex ([3b2d3b1](https://github.com/webpack/loader-utils/commit/3b2d3b1379b644aeefbbe859c0baeadd0b732710)) 11 | 12 | ## [3.3.0](https://github.com/webpack/loader-utils/compare/v3.2.2...v3.3.0) (2024-06-04) 13 | 14 | 15 | ### Features 16 | 17 | * add digestType 'base64safe' ([#259](https://github.com/webpack/loader-utils/issues/259)) ([af15793](https://github.com/webpack/loader-utils/commit/af157934abb1ee172cffd015acbabb065f0e1dbf)) 18 | 19 | ### [3.2.2](https://github.com/webpack/loader-utils/compare/v3.2.1...v3.2.2) (2024-05-29) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * unreachable code for directories ([128f945](https://github.com/webpack/loader-utils/commit/128f945e8f66d0ad7d69cdf568a8aa8bce40633a)) 25 | 26 | ### [3.2.1](https://github.com/webpack/loader-utils/compare/v3.2.0...v3.2.1) (2022-11-11) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * ReDoS problem ([#224](https://github.com/webpack/loader-utils/issues/224)) ([d2d752d](https://github.com/webpack/loader-utils/commit/d2d752d59629daee38f34b24307221349c490eb1)) 32 | 33 | ## [3.2.0](https://github.com/webpack/loader-utils/compare/v3.1.3...v3.2.0) (2021-11-11) 34 | 35 | 36 | ### Features 37 | 38 | * hash uniformity for base digests ([451858b](https://github.com/webpack/loader-utils/commit/451858b0bb33911d52d2f03a6470fd2b86493b84)) 39 | 40 | ### [3.1.3](https://github.com/webpack/loader-utils/compare/v3.1.2...v3.1.3) (2021-11-04) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * crash with md4 hash ([#198](https://github.com/webpack/loader-utils/issues/198)) ([ef084d4](https://github.com/webpack/loader-utils/commit/ef084d43ba29ebf3c3c0ea0939a5c58adad0bba2)) 46 | 47 | ### [3.1.2](https://github.com/webpack/loader-utils/compare/v3.1.1...v3.1.2) (2021-11-04) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * bug with unicode characters ([#196](https://github.com/webpack/loader-utils/issues/196)) ([0426405](https://github.com/webpack/loader-utils/commit/04264056f951514955af7302510631f942276eec)) 53 | 54 | ### [3.1.1](https://github.com/webpack/loader-utils/compare/v3.1.0...v3.1.1) (2021-11-04) 55 | 56 | 57 | ### Bug Fixes 58 | 59 | * base64 and unicode characters ([02b1f3f](https://github.com/webpack/loader-utils/commit/02b1f3fe6d718870b5ee7abc64519a1b2b5b8531)) 60 | 61 | ## [3.1.0](https://github.com/webpack/loader-utils/compare/v3.0.0...v3.1.0) (2021-10-29) 62 | 63 | 64 | ### Features 65 | 66 | * added `md4` (wasm version) and `md4-native` (`crypto` module version) algorithms ([cbf9d1d](https://github.com/webpack/loader-utils/commit/cbf9d1dac866be50971d294c3baacda45527fb8e)) 67 | 68 | ## [3.0.0](https://github.com/webpack/loader-utils/compare/v2.0.0...v3.0.0) (2021-10-20) 69 | 70 | 71 | ### ⚠ BREAKING CHANGES 72 | 73 | * minimum supported Node.js version is `12.13.0` ([93a87ce](https://github.com/webpack/loader-utils/commit/93a87cefd41cc69de0bc1f9099f7d753ed8cd557)) 74 | * use `xxhash64` by default for `[hash]`/`[contenthash]` and `getHashDigest` API 75 | * `[emoji]` was removed without replacements, please use custom function if you need this 76 | * removed `getOptions` in favor `loaderContext.getOptions` (`loaderContext` is `this` inside loader function), note - special query parameters like `?something=true` is not supported anymore, if you need this please do it on `loader` side, but we strongly recommend avoid it, as alternative you can use `?something=1` and handle `1` as `true` 77 | * removed `getRemainingRequest` in favor `loaderContext.remainingRequest` (`loaderContext` is `this` inside loader function) 78 | * removed `getCurrentRequest` in favor `loaderContext.currentRequest` (`loaderContext` is `this` inside loader function) 79 | * removed `parseString` in favor `JSON.parse` 80 | * removed `parseQuery` in favor `new URLSearchParams(loaderContext.resourceQuery.slice(1))` where `loaderContext` is `this` in loader function 81 | * removed `stringifyRequest` in favor `JSON.stringify(loaderContext.utils.contextify(loaderContext.context || loaderContext.rootContext, request))` (`loaderContext` is `this` inside loader function), also it will be cachable and faster 82 | * `isUrlRequest` ignores only absolute URLs and `#hash` requests, `data URI` and root relative request are handled as requestable due webpack v5 support them 83 | 84 | ### Bug Fixes 85 | 86 | * allowed the `interpolateName` API works without options ([862ea7d](https://github.com/webpack/loader-utils/commit/862ea7d1d0226558f2750bec36da02492d1e516d)) 87 | 88 | ## [2.0.0](https://github.com/webpack/loader-utils/compare/v1.4.0...v2.0.0) (2020-03-17) 89 | 90 | 91 | ### ⚠ BREAKING CHANGES 92 | 93 | * minimum required `Node.js` version is `8.9.0` ([#166](https://github.com/webpack/loader-utils/issues/166)) ([c937e8c](https://github.com/webpack/loader-utils/commit/c937e8c77231b42018be616b784a6b45eac86f8a)) 94 | * the `getOptions` method returns empty object on empty query ([#167](https://github.com/webpack/loader-utils/issues/167)) ([b595cfb](https://github.com/webpack/loader-utils/commit/b595cfba022d3f04f3d310dd570b0253e461605b)) 95 | * Use `md4` by default 96 | 97 | 98 | # [1.4.0](https://github.com/webpack/loader-utils/compare/v1.3.0...v1.4.0) (2020-02-19) 99 | 100 | 101 | ### Features 102 | 103 | * the `resourceQuery` is passed to the `interpolateName` method ([#163](https://github.com/webpack/loader-utils/issues/163)) ([cd0e428](https://github.com/webpack/loader-utils/commit/cd0e428)) 104 | 105 | 106 | 107 | 108 | # [1.3.0](https://github.com/webpack/loader-utils/compare/v1.2.3...v1.3.0) (2020-02-19) 109 | 110 | 111 | ### Features 112 | 113 | * support the `[query]` template for the `interpolatedName` method ([#162](https://github.com/webpack/loader-utils/issues/162)) ([469eeba](https://github.com/webpack/loader-utils/commit/469eeba)) 114 | 115 | 116 | 117 | 118 | ## [1.2.3](https://github.com/webpack/loader-utils/compare/v1.2.2...v1.2.3) (2018-12-27) 119 | 120 | 121 | ### Bug Fixes 122 | 123 | * **interpolateName:** don't interpolated `hashType` without `hash` or `contenthash` ([#140](https://github.com/webpack/loader-utils/issues/140)) ([3528fd9](https://github.com/webpack/loader-utils/commit/3528fd9)) 124 | 125 | 126 | 127 | 128 | ## [1.2.2](https://github.com/webpack/loader-utils/compare/v1.2.1...v1.2.2) (2018-12-27) 129 | 130 | 131 | ### Bug Fixes 132 | 133 | * fixed a hash type extracting in interpolateName ([#137](https://github.com/webpack/loader-utils/issues/137)) ([f8a71f4](https://github.com/webpack/loader-utils/commit/f8a71f4)) 134 | 135 | 136 | 137 | 138 | ## [1.2.1](https://github.com/webpack/loader-utils/compare/v1.2.0...v1.2.1) (2018-12-25) 139 | 140 | 141 | ### Bug Fixes 142 | 143 | * **isUrlRequest:** better handle absolute urls and non standards ([#134](https://github.com/webpack/loader-utils/issues/134)) ([aca43da](https://github.com/webpack/loader-utils/commit/aca43da)) 144 | 145 | 146 | ### Reverts 147 | 148 | * PR [#79](https://github.com/webpack/loader-utils/issues/79) ([#135](https://github.com/webpack/loader-utils/issues/135)) ([73d350a](https://github.com/webpack/loader-utils/commit/73d350a)) 149 | 150 | 151 | 152 | 153 | # [1.2.0](https://github.com/webpack/loader-utils/compare/v1.1.0...v1.2.0) (2018-12-24) 154 | 155 | 156 | ### Features 157 | 158 | * **interpolateName:** support `[contenthash]` 159 | 160 | ### Fixes 161 | 162 | * **urlToRequest:** empty urls are not rewritten to relative requests 163 | * **urlToRequest:** don't rewrite absolute urls 164 | * **isUrlRequest:** ignore all url with `extension` (like `moz-extension:`, `ms-browser-extension:` and etc) 165 | * **isUrlRequest:** ignore `about:blank` 166 | * **interpolateName:** failing explicitly when ran out of emoji 167 | * **interpolateName:** `[hash]` token regex in interpolate string to capture any hash algorithm name 168 | * **interpolateName:** parse string for emoji count before use 169 | 170 | 171 | 172 | 173 | # [1.1.0](https://github.com/webpack/loader-utils/compare/v1.0.4...v1.1.0) (2017-03-16) 174 | 175 | 176 | ### Features 177 | 178 | * **automatic-release:** Generation of automatic release ([7484d13](https://github.com/webpack/loader-utils/commit/7484d13)) 179 | * **parseQuery:** export parseQuery ([ddf64e4](https://github.com/webpack/loader-utils/commit/ddf64e4)) 180 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loader-utils 2 | 3 | ## Methods 4 | 5 | ### `urlToRequest` 6 | 7 | Converts some resource URL to a webpack module request. 8 | 9 | > i Before call `urlToRequest` you need call `isUrlRequest` to ensure it is requestable url 10 | 11 | ```javascript 12 | const url = "path/to/module.js"; 13 | 14 | if (loaderUtils.isUrlRequest(url)) { 15 | // Logic for requestable url 16 | const request = loaderUtils.urlToRequest(url); 17 | } else { 18 | // Logic for not requestable url 19 | } 20 | ``` 21 | 22 | Simple example: 23 | 24 | ```javascript 25 | const url = "path/to/module.js"; 26 | const request = loaderUtils.urlToRequest(url); // "./path/to/module.js" 27 | ``` 28 | 29 | #### Module URLs 30 | 31 | Any URL containing a `~` will be interpreted as a module request. Anything after the `~` will be considered the request path. 32 | 33 | ```javascript 34 | const url = "~path/to/module.js"; 35 | const request = loaderUtils.urlToRequest(url); // "path/to/module.js" 36 | ``` 37 | 38 | #### Root-relative URLs 39 | 40 | URLs that are root-relative (start with `/`) can be resolved relative to some arbitrary path by using the `root` parameter: 41 | 42 | ```javascript 43 | const url = "/path/to/module.js"; 44 | const root = "./root"; 45 | const request = loaderUtils.urlToRequest(url, root); // "./root/path/to/module.js" 46 | ``` 47 | 48 | To convert a root-relative URL into a module URL, specify a `root` value that starts with `~`: 49 | 50 | ```javascript 51 | const url = "/path/to/module.js"; 52 | const root = "~"; 53 | const request = loaderUtils.urlToRequest(url, root); // "path/to/module.js" 54 | ``` 55 | 56 | ### `interpolateName` 57 | 58 | Interpolates a filename template using multiple placeholders and/or a regular expression. 59 | The template and regular expression are set as query params called `name` and `regExp` on the current loader's context. 60 | 61 | ```javascript 62 | const interpolatedName = loaderUtils.interpolateName( 63 | loaderContext, 64 | name, 65 | options 66 | ); 67 | ``` 68 | 69 | The following tokens are replaced in the `name` parameter: 70 | 71 | - `[ext]` the extension of the resource 72 | - `[name]` the basename of the resource 73 | - `[path]` the path of the resource relative to the `context` query parameter or option. 74 | - `[folder]` the folder the resource is in 75 | - `[query]` the queryof the resource, i.e. `?foo=bar` 76 | - `[contenthash]` the hash of `options.content` (Buffer) (by default it's the hex digest of the `xxhash64` hash) 77 | - `[:contenthash::]` optionally one can configure 78 | - other `hashType`s, i. e. `xxhash64`, `sha1`, `md4` (wasm version), `native-md4` (`crypto` module version), `md5`, `sha256`, `sha512` 79 | - other `digestType`s, i. e. `hex`, `base26`, `base32`, `base36`, `base49`, `base52`, `base58`, `base62`, `base64`, `base64safe` 80 | - and `length` the length in chars 81 | - `[hash]` the hash of `options.content` (Buffer) (by default it's the hex digest of the `xxhash64` hash) 82 | - `[:hash::]` optionally one can configure 83 | - other `hashType`s, i. e. `xxhash64`, `sha1`, `md4` (wasm version), `native-md4` (`crypto` module version), `md5`, `sha256`, `sha512` 84 | - other `digestType`s, i. e. `hex`, `base26`, `base32`, `base36`, `base49`, `base52`, `base58`, `base62`, `base64`, `base64safe` 85 | - and `length` the length in chars 86 | - `[N]` the N-th match obtained from matching the current file name against `options.regExp` 87 | 88 | In loader context `[hash]` and `[contenthash]` are the same, but we recommend using `[contenthash]` for avoid misleading. 89 | 90 | `digestType` with `base64safe` don't contain `/`, `+` and `=` symbols. 91 | 92 | Examples 93 | 94 | ```javascript 95 | // loaderContext.resourcePath = "/absolute/path/to/app/js/javascript.js" 96 | loaderUtils.interpolateName(loaderContext, "js/[hash].script.[ext]", { content: ... }); 97 | // => js/9473fdd0d880a43c21b7778d34872157.script.js 98 | 99 | // loaderContext.resourcePath = "/absolute/path/to/app/js/javascript.js" 100 | // loaderContext.resourceQuery = "?foo=bar" 101 | loaderUtils.interpolateName(loaderContext, "js/[hash].script.[ext][query]", { content: ... }); 102 | // => js/9473fdd0d880a43c21b7778d34872157.script.js?foo=bar 103 | 104 | // loaderContext.resourcePath = "/absolute/path/to/app/js/javascript.js" 105 | loaderUtils.interpolateName(loaderContext, "js/[contenthash].script.[ext]", { content: ... }); 106 | // => js/9473fdd0d880a43c21b7778d34872157.script.js 107 | 108 | // loaderContext.resourcePath = "/absolute/path/to/app/page.html" 109 | loaderUtils.interpolateName(loaderContext, "html-[hash:6].html", { content: ... }); 110 | // => html-9473fd.html 111 | 112 | // loaderContext.resourcePath = "/absolute/path/to/app/flash.txt" 113 | loaderUtils.interpolateName(loaderContext, "[hash]", { content: ... }); 114 | // => c31e9820c001c9c4a86bce33ce43b679 115 | 116 | // loaderContext.resourcePath = "/absolute/path/to/app/img/image.png" 117 | loaderUtils.interpolateName(loaderContext, "[sha512:hash:base64:7].[ext]", { content: ... }); 118 | // => 2BKDTjl.png 119 | // use sha512 hash instead of xxhash64 and with only 7 chars of base64 120 | 121 | // loaderContext.resourcePath = "/absolute/path/to/app/img/myself.png" 122 | // loaderContext.query.name = 123 | loaderUtils.interpolateName(loaderContext, "picture.png"); 124 | // => picture.png 125 | 126 | // loaderContext.resourcePath = "/absolute/path/to/app/dir/file.png" 127 | loaderUtils.interpolateName(loaderContext, "[path][name].[ext]?[hash]", { content: ... }); 128 | // => /app/dir/file.png?9473fdd0d880a43c21b7778d34872157 129 | 130 | // loaderContext.resourcePath = "/absolute/path/to/app/js/page-home.js" 131 | loaderUtils.interpolateName(loaderContext, "script-[1].[ext]", { regExp: "page-(.*)\\.js", content: ... }); 132 | // => script-home.js 133 | 134 | // loaderContext.resourcePath = "/absolute/path/to/app/js/javascript.js" 135 | // loaderContext.resourceQuery = "?foo=bar" 136 | loaderUtils.interpolateName( 137 | loaderContext, 138 | (resourcePath, resourceQuery) => { 139 | // resourcePath - `/app/js/javascript.js` 140 | // resourceQuery - `?foo=bar` 141 | 142 | return "js/[hash].script.[ext]"; 143 | }, 144 | { content: ... } 145 | ); 146 | // => js/9473fdd0d880a43c21b7778d34872157.script.js 147 | ``` 148 | 149 | ### `getHashDigest` 150 | 151 | ```javascript 152 | const digestString = loaderUtils.getHashDigest( 153 | buffer, 154 | hashType, 155 | digestType, 156 | maxLength 157 | ); 158 | ``` 159 | 160 | - `buffer` the content that should be hashed 161 | - `hashType` one of `xxhash64`, `sha1`, `md4`, `md5`, `sha256`, `sha512` or any other node.js supported hash type 162 | - `digestType` one of `hex`, `base26`, `base32`, `base36`, `base49`, `base52`, `base58`, `base62`, `base64`, `base64safe` 163 | - `maxLength` the maximum length in chars 164 | 165 | ## License 166 | 167 | MIT (http://www.opensource.org/licenses/mit-license.php) 168 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting Security Issues 4 | 5 | If you discover a security issue in webpack/loader-utils, please report it by following the instructions 6 | here: https://github.com/webpack/webpack/blob/main/SECURITY.md 7 | -------------------------------------------------------------------------------- /lib/getHashDigest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const baseEncodeTables = { 4 | 26: "abcdefghijklmnopqrstuvwxyz", 5 | 32: "123456789abcdefghjkmnpqrstuvwxyz", // no 0lio 6 | 36: "0123456789abcdefghijklmnopqrstuvwxyz", 7 | 49: "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ", // no lIO 8 | 52: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 9 | 58: "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ", // no 0lIO 10 | 62: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 11 | 64: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_", 12 | }; 13 | 14 | /** 15 | * @param {Uint32Array} uint32Array Treated as a long base-0x100000000 number, little endian 16 | * @param {number} divisor The divisor 17 | * @return {number} Modulo (remainder) of the division 18 | */ 19 | function divmod32(uint32Array, divisor) { 20 | let carry = 0; 21 | for (let i = uint32Array.length - 1; i >= 0; i--) { 22 | const value = carry * 0x100000000 + uint32Array[i]; 23 | carry = value % divisor; 24 | uint32Array[i] = Math.floor(value / divisor); 25 | } 26 | return carry; 27 | } 28 | 29 | function encodeBufferToBase(buffer, base, length) { 30 | const encodeTable = baseEncodeTables[base]; 31 | 32 | if (!encodeTable) { 33 | throw new Error("Unknown encoding base" + base); 34 | } 35 | 36 | // Input bits are only enough to generate this many characters 37 | const limit = Math.ceil((buffer.length * 8) / Math.log2(base)); 38 | length = Math.min(length, limit); 39 | 40 | // Most of the crypto digests (if not all) has length a multiple of 4 bytes. 41 | // Fewer numbers in the array means faster math. 42 | const uint32Array = new Uint32Array(Math.ceil(buffer.length / 4)); 43 | 44 | // Make sure the input buffer data is copied and is not mutated by reference. 45 | // divmod32() would corrupt the BulkUpdateDecorator cache otherwise. 46 | buffer.copy(Buffer.from(uint32Array.buffer)); 47 | 48 | let output = ""; 49 | 50 | for (let i = 0; i < length; i++) { 51 | output = encodeTable[divmod32(uint32Array, base)] + output; 52 | } 53 | 54 | return output; 55 | } 56 | 57 | let crypto = undefined; 58 | let createXXHash64 = undefined; 59 | let createMd4 = undefined; 60 | let BatchedHash = undefined; 61 | let BulkUpdateDecorator = undefined; 62 | 63 | function getHashDigest(buffer, algorithm, digestType, maxLength) { 64 | algorithm = algorithm || "xxhash64"; 65 | maxLength = maxLength || 9999; 66 | 67 | let hash; 68 | 69 | if (algorithm === "xxhash64") { 70 | if (createXXHash64 === undefined) { 71 | createXXHash64 = require("./hash/xxhash64"); 72 | 73 | if (BatchedHash === undefined) { 74 | BatchedHash = require("./hash/BatchedHash"); 75 | } 76 | } 77 | 78 | hash = new BatchedHash(createXXHash64()); 79 | } else if (algorithm === "md4") { 80 | if (createMd4 === undefined) { 81 | createMd4 = require("./hash/md4"); 82 | 83 | if (BatchedHash === undefined) { 84 | BatchedHash = require("./hash/BatchedHash"); 85 | } 86 | } 87 | 88 | hash = new BatchedHash(createMd4()); 89 | } else if (algorithm === "native-md4") { 90 | if (typeof crypto === "undefined") { 91 | crypto = require("crypto"); 92 | 93 | if (BulkUpdateDecorator === undefined) { 94 | BulkUpdateDecorator = require("./hash/BulkUpdateDecorator"); 95 | } 96 | } 97 | 98 | hash = new BulkUpdateDecorator(() => crypto.createHash("md4"), "md4"); 99 | } else { 100 | if (typeof crypto === "undefined") { 101 | crypto = require("crypto"); 102 | 103 | if (BulkUpdateDecorator === undefined) { 104 | BulkUpdateDecorator = require("./hash/BulkUpdateDecorator"); 105 | } 106 | } 107 | 108 | hash = new BulkUpdateDecorator( 109 | () => crypto.createHash(algorithm), 110 | algorithm 111 | ); 112 | } 113 | 114 | hash.update(buffer); 115 | 116 | if ( 117 | digestType === "base26" || 118 | digestType === "base32" || 119 | digestType === "base36" || 120 | digestType === "base49" || 121 | digestType === "base52" || 122 | digestType === "base58" || 123 | digestType === "base62" || 124 | digestType === "base64safe" 125 | ) { 126 | return encodeBufferToBase( 127 | hash.digest(), 128 | digestType === "base64safe" ? 64 : digestType.substr(4), 129 | maxLength 130 | ); 131 | } 132 | 133 | return hash.digest(digestType || "hex").substr(0, maxLength); 134 | } 135 | 136 | module.exports = getHashDigest; 137 | -------------------------------------------------------------------------------- /lib/hash/BatchedHash.js: -------------------------------------------------------------------------------- 1 | const MAX_SHORT_STRING = require("./wasm-hash").MAX_SHORT_STRING; 2 | 3 | class BatchedHash { 4 | constructor(hash) { 5 | this.string = undefined; 6 | this.encoding = undefined; 7 | this.hash = hash; 8 | } 9 | 10 | /** 11 | * Update hash {@link https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding} 12 | * @param {string|Buffer} data data 13 | * @param {string=} inputEncoding data encoding 14 | * @returns {this} updated hash 15 | */ 16 | update(data, inputEncoding) { 17 | if (this.string !== undefined) { 18 | if ( 19 | typeof data === "string" && 20 | inputEncoding === this.encoding && 21 | this.string.length + data.length < MAX_SHORT_STRING 22 | ) { 23 | this.string += data; 24 | 25 | return this; 26 | } 27 | 28 | this.hash.update(this.string, this.encoding); 29 | this.string = undefined; 30 | } 31 | 32 | if (typeof data === "string") { 33 | if ( 34 | data.length < MAX_SHORT_STRING && 35 | // base64 encoding is not valid since it may contain padding chars 36 | (!inputEncoding || !inputEncoding.startsWith("ba")) 37 | ) { 38 | this.string = data; 39 | this.encoding = inputEncoding; 40 | } else { 41 | this.hash.update(data, inputEncoding); 42 | } 43 | } else { 44 | this.hash.update(data); 45 | } 46 | 47 | return this; 48 | } 49 | 50 | /** 51 | * Calculates the digest {@link https://nodejs.org/api/crypto.html#crypto_hash_digest_encoding} 52 | * @param {string=} encoding encoding of the return value 53 | * @returns {string|Buffer} digest 54 | */ 55 | digest(encoding) { 56 | if (this.string !== undefined) { 57 | this.hash.update(this.string, this.encoding); 58 | } 59 | 60 | return this.hash.digest(encoding); 61 | } 62 | } 63 | 64 | module.exports = BatchedHash; 65 | -------------------------------------------------------------------------------- /lib/hash/BulkUpdateDecorator.js: -------------------------------------------------------------------------------- 1 | const BULK_SIZE = 2000; 2 | 3 | // We are using an object instead of a Map as this will stay static during the runtime 4 | // so access to it can be optimized by v8 5 | const digestCaches = {}; 6 | 7 | class BulkUpdateDecorator { 8 | /** 9 | * @param {Hash | function(): Hash} hashOrFactory function to create a hash 10 | * @param {string=} hashKey key for caching 11 | */ 12 | constructor(hashOrFactory, hashKey) { 13 | this.hashKey = hashKey; 14 | 15 | if (typeof hashOrFactory === "function") { 16 | this.hashFactory = hashOrFactory; 17 | this.hash = undefined; 18 | } else { 19 | this.hashFactory = undefined; 20 | this.hash = hashOrFactory; 21 | } 22 | 23 | this.buffer = ""; 24 | } 25 | 26 | /** 27 | * Update hash {@link https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding} 28 | * @param {string|Buffer} data data 29 | * @param {string=} inputEncoding data encoding 30 | * @returns {this} updated hash 31 | */ 32 | update(data, inputEncoding) { 33 | if ( 34 | inputEncoding !== undefined || 35 | typeof data !== "string" || 36 | data.length > BULK_SIZE 37 | ) { 38 | if (this.hash === undefined) { 39 | this.hash = this.hashFactory(); 40 | } 41 | 42 | if (this.buffer.length > 0) { 43 | this.hash.update(this.buffer); 44 | this.buffer = ""; 45 | } 46 | 47 | this.hash.update(data, inputEncoding); 48 | } else { 49 | this.buffer += data; 50 | 51 | if (this.buffer.length > BULK_SIZE) { 52 | if (this.hash === undefined) { 53 | this.hash = this.hashFactory(); 54 | } 55 | 56 | this.hash.update(this.buffer); 57 | this.buffer = ""; 58 | } 59 | } 60 | 61 | return this; 62 | } 63 | 64 | /** 65 | * Calculates the digest {@link https://nodejs.org/api/crypto.html#crypto_hash_digest_encoding} 66 | * @param {string=} encoding encoding of the return value 67 | * @returns {string|Buffer} digest 68 | */ 69 | digest(encoding) { 70 | let digestCache; 71 | 72 | const buffer = this.buffer; 73 | 74 | if (this.hash === undefined) { 75 | // short data for hash, we can use caching 76 | const cacheKey = `${this.hashKey}-${encoding}`; 77 | 78 | digestCache = digestCaches[cacheKey]; 79 | 80 | if (digestCache === undefined) { 81 | digestCache = digestCaches[cacheKey] = new Map(); 82 | } 83 | 84 | const cacheEntry = digestCache.get(buffer); 85 | 86 | if (cacheEntry !== undefined) { 87 | return cacheEntry; 88 | } 89 | 90 | this.hash = this.hashFactory(); 91 | } 92 | 93 | if (buffer.length > 0) { 94 | this.hash.update(buffer); 95 | } 96 | 97 | const digestResult = this.hash.digest(encoding); 98 | 99 | if (digestCache !== undefined) { 100 | digestCache.set(buffer, digestResult); 101 | } 102 | 103 | return digestResult; 104 | } 105 | } 106 | 107 | module.exports = BulkUpdateDecorator; 108 | -------------------------------------------------------------------------------- /lib/hash/md4.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License http://www.opensource.org/licenses/mit-license.php 3 | Author Tobias Koppers @sokra 4 | */ 5 | 6 | "use strict"; 7 | 8 | const create = require("./wasm-hash"); 9 | 10 | //#region wasm code: md4 (../../../assembly/hash/md4.asm.ts) --initialMemory 1 11 | const md4 = new WebAssembly.Module( 12 | Buffer.from( 13 | // 2150 bytes 14 | "AGFzbQEAAAABCAJgAX8AYAAAAwUEAQAAAAUDAQABBhoFfwFBAAt/AUEAC38BQQALfwFBAAt/AUEACwciBARpbml0AAAGdXBkYXRlAAIFZmluYWwAAwZtZW1vcnkCAAqFEAQmAEGBxpS6BiQBQYnXtv5+JAJB/rnrxXkkA0H2qMmBASQEQQAkAAvMCgEYfyMBIQojAiEGIwMhByMEIQgDQCAAIAVLBEAgBSgCCCINIAcgBiAFKAIEIgsgCCAHIAUoAgAiDCAKIAggBiAHIAhzcXNqakEDdyIDIAYgB3Nxc2pqQQd3IgEgAyAGc3FzampBC3chAiAFKAIUIg8gASACIAUoAhAiCSADIAEgBSgCDCIOIAYgAyACIAEgA3Nxc2pqQRN3IgQgASACc3FzampBA3ciAyACIARzcXNqakEHdyEBIAUoAiAiEiADIAEgBSgCHCIRIAQgAyAFKAIYIhAgAiAEIAEgAyAEc3FzampBC3ciAiABIANzcXNqakETdyIEIAEgAnNxc2pqQQN3IQMgBSgCLCIVIAQgAyAFKAIoIhQgAiAEIAUoAiQiEyABIAIgAyACIARzcXNqakEHdyIBIAMgBHNxc2pqQQt3IgIgASADc3FzampBE3chBCAPIBAgCSAVIBQgEyAFKAI4IhYgAiAEIAUoAjQiFyABIAIgBSgCMCIYIAMgASAEIAEgAnNxc2pqQQN3IgEgAiAEc3FzampBB3ciAiABIARzcXNqakELdyIDIAkgAiAMIAEgBSgCPCIJIAQgASADIAEgAnNxc2pqQRN3IgEgAiADcnEgAiADcXJqakGZ84nUBWpBA3ciAiABIANycSABIANxcmpqQZnzidQFakEFdyIEIAEgAnJxIAEgAnFyaiASakGZ84nUBWpBCXciAyAPIAQgCyACIBggASADIAIgBHJxIAIgBHFyampBmfOJ1AVqQQ13IgEgAyAEcnEgAyAEcXJqakGZ84nUBWpBA3ciAiABIANycSABIANxcmpqQZnzidQFakEFdyIEIAEgAnJxIAEgAnFyampBmfOJ1AVqQQl3IgMgECAEIAIgFyABIAMgAiAEcnEgAiAEcXJqakGZ84nUBWpBDXciASADIARycSADIARxcmogDWpBmfOJ1AVqQQN3IgIgASADcnEgASADcXJqakGZ84nUBWpBBXciBCABIAJycSABIAJxcmpqQZnzidQFakEJdyIDIBEgBCAOIAIgFiABIAMgAiAEcnEgAiAEcXJqakGZ84nUBWpBDXciASADIARycSADIARxcmpqQZnzidQFakEDdyICIAEgA3JxIAEgA3FyampBmfOJ1AVqQQV3IgQgASACcnEgASACcXJqakGZ84nUBWpBCXciAyAMIAIgAyAJIAEgAyACIARycSACIARxcmpqQZnzidQFakENdyIBcyAEc2pqQaHX5/YGakEDdyICIAQgASACcyADc2ogEmpBodfn9gZqQQl3IgRzIAFzampBodfn9gZqQQt3IgMgAiADIBggASADIARzIAJzampBodfn9gZqQQ93IgFzIARzaiANakGh1+f2BmpBA3ciAiAUIAQgASACcyADc2pqQaHX5/YGakEJdyIEcyABc2pqQaHX5/YGakELdyIDIAsgAiADIBYgASADIARzIAJzampBodfn9gZqQQ93IgFzIARzampBodfn9gZqQQN3IgIgEyAEIAEgAnMgA3NqakGh1+f2BmpBCXciBHMgAXNqakGh1+f2BmpBC3chAyAKIA4gAiADIBcgASADIARzIAJzampBodfn9gZqQQ93IgFzIARzampBodfn9gZqQQN3IgJqIQogBiAJIAEgESADIAIgFSAEIAEgAnMgA3NqakGh1+f2BmpBCXciBHMgAXNqakGh1+f2BmpBC3ciAyAEcyACc2pqQaHX5/YGakEPd2ohBiADIAdqIQcgBCAIaiEIIAVBQGshBQwBCwsgCiQBIAYkAiAHJAMgCCQECw0AIAAQASMAIABqJAAL/wQCA38BfiMAIABqrUIDhiEEIABByABqQUBxIgJBCGshAyAAIgFBAWohACABQYABOgAAA0AgACACSUEAIABBB3EbBEAgAEEAOgAAIABBAWohAAwBCwsDQCAAIAJJBEAgAEIANwMAIABBCGohAAwBCwsgAyAENwMAIAIQAUEAIwGtIgRC//8DgyAEQoCA/P8Pg0IQhoQiBEL/gYCA8B+DIARCgP6DgIDgP4NCCIaEIgRCj4C8gPCBwAeDQgiGIARC8IHAh4CegPgAg0IEiIQiBEKGjJiw4MCBgwZ8QgSIQoGChIiQoMCAAYNCJ34gBEKw4MCBg4aMmDCEfDcDAEEIIwKtIgRC//8DgyAEQoCA/P8Pg0IQhoQiBEL/gYCA8B+DIARCgP6DgIDgP4NCCIaEIgRCj4C8gPCBwAeDQgiGIARC8IHAh4CegPgAg0IEiIQiBEKGjJiw4MCBgwZ8QgSIQoGChIiQoMCAAYNCJ34gBEKw4MCBg4aMmDCEfDcDAEEQIwOtIgRC//8DgyAEQoCA/P8Pg0IQhoQiBEL/gYCA8B+DIARCgP6DgIDgP4NCCIaEIgRCj4C8gPCBwAeDQgiGIARC8IHAh4CegPgAg0IEiIQiBEKGjJiw4MCBgwZ8QgSIQoGChIiQoMCAAYNCJ34gBEKw4MCBg4aMmDCEfDcDAEEYIwStIgRC//8DgyAEQoCA/P8Pg0IQhoQiBEL/gYCA8B+DIARCgP6DgIDgP4NCCIaEIgRCj4C8gPCBwAeDQgiGIARC8IHAh4CegPgAg0IEiIQiBEKGjJiw4MCBgwZ8QgSIQoGChIiQoMCAAYNCJ34gBEKw4MCBg4aMmDCEfDcDAAs=", 15 | "base64" 16 | ) 17 | ); 18 | //#endregion 19 | 20 | module.exports = create.bind(null, md4, [], 64, 32); 21 | -------------------------------------------------------------------------------- /lib/hash/wasm-hash.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License http://www.opensource.org/licenses/mit-license.php 3 | Author Tobias Koppers @sokra 4 | */ 5 | 6 | "use strict"; 7 | 8 | // 65536 is the size of a wasm memory page 9 | // 64 is the maximum chunk size for every possible wasm hash implementation 10 | // 4 is the maximum number of bytes per char for string encoding (max is utf-8) 11 | // ~3 makes sure that it's always a block of 4 chars, so avoid partially encoded bytes for base64 12 | const MAX_SHORT_STRING = Math.floor((65536 - 64) / 4) & ~3; 13 | 14 | class WasmHash { 15 | /** 16 | * @param {WebAssembly.Instance} instance wasm instance 17 | * @param {WebAssembly.Instance[]} instancesPool pool of instances 18 | * @param {number} chunkSize size of data chunks passed to wasm 19 | * @param {number} digestSize size of digest returned by wasm 20 | */ 21 | constructor(instance, instancesPool, chunkSize, digestSize) { 22 | const exports = /** @type {any} */ (instance.exports); 23 | 24 | exports.init(); 25 | 26 | this.exports = exports; 27 | this.mem = Buffer.from(exports.memory.buffer, 0, 65536); 28 | this.buffered = 0; 29 | this.instancesPool = instancesPool; 30 | this.chunkSize = chunkSize; 31 | this.digestSize = digestSize; 32 | } 33 | 34 | reset() { 35 | this.buffered = 0; 36 | this.exports.init(); 37 | } 38 | 39 | /** 40 | * @param {Buffer | string} data data 41 | * @param {BufferEncoding=} encoding encoding 42 | * @returns {this} itself 43 | */ 44 | update(data, encoding) { 45 | if (typeof data === "string") { 46 | while (data.length > MAX_SHORT_STRING) { 47 | this._updateWithShortString(data.slice(0, MAX_SHORT_STRING), encoding); 48 | data = data.slice(MAX_SHORT_STRING); 49 | } 50 | 51 | this._updateWithShortString(data, encoding); 52 | 53 | return this; 54 | } 55 | 56 | this._updateWithBuffer(data); 57 | 58 | return this; 59 | } 60 | 61 | /** 62 | * @param {string} data data 63 | * @param {BufferEncoding=} encoding encoding 64 | * @returns {void} 65 | */ 66 | _updateWithShortString(data, encoding) { 67 | const { exports, buffered, mem, chunkSize } = this; 68 | 69 | let endPos; 70 | 71 | if (data.length < 70) { 72 | if (!encoding || encoding === "utf-8" || encoding === "utf8") { 73 | endPos = buffered; 74 | for (let i = 0; i < data.length; i++) { 75 | const cc = data.charCodeAt(i); 76 | 77 | if (cc < 0x80) { 78 | mem[endPos++] = cc; 79 | } else if (cc < 0x800) { 80 | mem[endPos] = (cc >> 6) | 0xc0; 81 | mem[endPos + 1] = (cc & 0x3f) | 0x80; 82 | endPos += 2; 83 | } else { 84 | // bail-out for weird chars 85 | endPos += mem.write(data.slice(i), endPos, encoding); 86 | break; 87 | } 88 | } 89 | } else if (encoding === "latin1") { 90 | endPos = buffered; 91 | 92 | for (let i = 0; i < data.length; i++) { 93 | const cc = data.charCodeAt(i); 94 | 95 | mem[endPos++] = cc; 96 | } 97 | } else { 98 | endPos = buffered + mem.write(data, buffered, encoding); 99 | } 100 | } else { 101 | endPos = buffered + mem.write(data, buffered, encoding); 102 | } 103 | 104 | if (endPos < chunkSize) { 105 | this.buffered = endPos; 106 | } else { 107 | const l = endPos & ~(this.chunkSize - 1); 108 | 109 | exports.update(l); 110 | 111 | const newBuffered = endPos - l; 112 | 113 | this.buffered = newBuffered; 114 | 115 | if (newBuffered > 0) { 116 | mem.copyWithin(0, l, endPos); 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * @param {Buffer} data data 123 | * @returns {void} 124 | */ 125 | _updateWithBuffer(data) { 126 | const { exports, buffered, mem } = this; 127 | const length = data.length; 128 | 129 | if (buffered + length < this.chunkSize) { 130 | data.copy(mem, buffered, 0, length); 131 | 132 | this.buffered += length; 133 | } else { 134 | const l = (buffered + length) & ~(this.chunkSize - 1); 135 | 136 | if (l > 65536) { 137 | let i = 65536 - buffered; 138 | 139 | data.copy(mem, buffered, 0, i); 140 | exports.update(65536); 141 | 142 | const stop = l - buffered - 65536; 143 | 144 | while (i < stop) { 145 | data.copy(mem, 0, i, i + 65536); 146 | exports.update(65536); 147 | i += 65536; 148 | } 149 | 150 | data.copy(mem, 0, i, l - buffered); 151 | 152 | exports.update(l - buffered - i); 153 | } else { 154 | data.copy(mem, buffered, 0, l - buffered); 155 | 156 | exports.update(l); 157 | } 158 | 159 | const newBuffered = length + buffered - l; 160 | 161 | this.buffered = newBuffered; 162 | 163 | if (newBuffered > 0) { 164 | data.copy(mem, 0, length - newBuffered, length); 165 | } 166 | } 167 | } 168 | 169 | digest(type) { 170 | const { exports, buffered, mem, digestSize } = this; 171 | 172 | exports.final(buffered); 173 | 174 | this.instancesPool.push(this); 175 | 176 | const hex = mem.toString("latin1", 0, digestSize); 177 | 178 | if (type === "hex") { 179 | return hex; 180 | } 181 | 182 | if (type === "binary" || !type) { 183 | return Buffer.from(hex, "hex"); 184 | } 185 | 186 | return Buffer.from(hex, "hex").toString(type); 187 | } 188 | } 189 | 190 | const create = (wasmModule, instancesPool, chunkSize, digestSize) => { 191 | if (instancesPool.length > 0) { 192 | const old = instancesPool.pop(); 193 | 194 | old.reset(); 195 | 196 | return old; 197 | } else { 198 | return new WasmHash( 199 | new WebAssembly.Instance(wasmModule), 200 | instancesPool, 201 | chunkSize, 202 | digestSize 203 | ); 204 | } 205 | }; 206 | 207 | module.exports = create; 208 | module.exports.MAX_SHORT_STRING = MAX_SHORT_STRING; 209 | -------------------------------------------------------------------------------- /lib/hash/xxhash64.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License http://www.opensource.org/licenses/mit-license.php 3 | Author Tobias Koppers @sokra 4 | */ 5 | 6 | "use strict"; 7 | 8 | const create = require("./wasm-hash"); 9 | 10 | //#region wasm code: xxhash64 (../../../assembly/hash/xxhash64.asm.ts) --initialMemory 1 11 | const xxhash64 = new WebAssembly.Module( 12 | Buffer.from( 13 | // 1173 bytes 14 | "AGFzbQEAAAABCAJgAX8AYAAAAwQDAQAABQMBAAEGGgV+AUIAC34BQgALfgFCAAt+AUIAC34BQgALByIEBGluaXQAAAZ1cGRhdGUAAQVmaW5hbAACBm1lbW9yeQIACrUIAzAAQtbrgu7q/Yn14AAkAELP1tO+0ser2UIkAUIAJAJC+erQ0OfJoeThACQDQgAkBAvUAQIBfwR+IABFBEAPCyMEIACtfCQEIwAhAiMBIQMjAiEEIwMhBQNAIAIgASkDAELP1tO+0ser2UJ+fEIfiUKHla+vmLbem55/fiECIAMgASkDCELP1tO+0ser2UJ+fEIfiUKHla+vmLbem55/fiEDIAQgASkDEELP1tO+0ser2UJ+fEIfiUKHla+vmLbem55/fiEEIAUgASkDGELP1tO+0ser2UJ+fEIfiUKHla+vmLbem55/fiEFIAAgAUEgaiIBSw0ACyACJAAgAyQBIAQkAiAFJAMLqwYCAX8EfiMEQgBSBH4jACICQgGJIwEiA0IHiXwjAiIEQgyJfCMDIgVCEol8IAJCz9bTvtLHq9lCfkIfiUKHla+vmLbem55/foVCh5Wvr5i23puef35CnaO16oOxjYr6AH0gA0LP1tO+0ser2UJ+Qh+JQoeVr6+Ytt6bnn9+hUKHla+vmLbem55/fkKdo7Xqg7GNivoAfSAEQs/W077Sx6vZQn5CH4lCh5Wvr5i23puef36FQoeVr6+Ytt6bnn9+Qp2jteqDsY2K+gB9IAVCz9bTvtLHq9lCfkIfiUKHla+vmLbem55/foVCh5Wvr5i23puef35CnaO16oOxjYr6AH0FQsXP2bLx5brqJwsjBCAArXx8IQIDQCABQQhqIABNBEAgAiABKQMAQs/W077Sx6vZQn5CH4lCh5Wvr5i23puef36FQhuJQoeVr6+Ytt6bnn9+Qp2jteqDsY2K+gB9IQIgAUEIaiEBDAELCyABQQRqIABNBEACfyACIAE1AgBCh5Wvr5i23puef36FQheJQs/W077Sx6vZQn5C+fPd8Zn2masWfCECIAFBBGoLIQELA0AgACABRwRAIAIgATEAAELFz9my8eW66id+hUILiUKHla+vmLbem55/fiECIAFBAWohAQwBCwtBACACIAJCIYiFQs/W077Sx6vZQn4iAiACQh2IhUL5893xmfaZqxZ+IgIgAkIgiIUiAkIgiCIDQv//A4NCIIYgA0KAgPz/D4NCEIiEIgNC/4GAgPAfg0IQhiADQoD+g4CA4D+DQgiIhCIDQo+AvIDwgcAHg0IIhiADQvCBwIeAnoD4AINCBIiEIgNChoyYsODAgYMGfEIEiEKBgoSIkKDAgAGDQid+IANCsODAgYOGjJgwhHw3AwBBCCACQv////8PgyICQv//A4NCIIYgAkKAgPz/D4NCEIiEIgJC/4GAgPAfg0IQhiACQoD+g4CA4D+DQgiIhCICQo+AvIDwgcAHg0IIhiACQvCBwIeAnoD4AINCBIiEIgJChoyYsODAgYMGfEIEiEKBgoSIkKDAgAGDQid+IAJCsODAgYOGjJgwhHw3AwAL", 15 | "base64" 16 | ) 17 | ); 18 | //#endregion 19 | 20 | module.exports = create.bind(null, xxhash64, [], 32, 16); 21 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const isUrlRequest = require("./isUrlRequest"); 4 | const urlToRequest = require("./urlToRequest"); 5 | const getHashDigest = require("./getHashDigest"); 6 | const interpolateName = require("./interpolateName"); 7 | 8 | exports.urlToRequest = urlToRequest; 9 | exports.getHashDigest = getHashDigest; 10 | exports.interpolateName = interpolateName; 11 | exports.isUrlRequest = isUrlRequest; 12 | -------------------------------------------------------------------------------- /lib/interpolateName.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const getHashDigest = require("./getHashDigest"); 5 | 6 | function interpolateName(loaderContext, name, options = {}) { 7 | let filename; 8 | 9 | const hasQuery = 10 | loaderContext.resourceQuery && loaderContext.resourceQuery.length > 1; 11 | 12 | if (typeof name === "function") { 13 | filename = name( 14 | loaderContext.resourcePath, 15 | hasQuery ? loaderContext.resourceQuery : undefined 16 | ); 17 | } else { 18 | filename = name || "[hash].[ext]"; 19 | } 20 | 21 | const context = options.context; 22 | const content = options.content; 23 | const regExp = options.regExp; 24 | 25 | let ext = "bin"; 26 | let basename = "file"; 27 | let directory = ""; 28 | let folder = ""; 29 | let query = ""; 30 | 31 | if (loaderContext.resourcePath) { 32 | const parsed = path.parse(loaderContext.resourcePath); 33 | let resourcePath = loaderContext.resourcePath; 34 | 35 | if (parsed.ext) { 36 | ext = parsed.ext.substr(1); 37 | } 38 | 39 | if (parsed.dir) { 40 | basename = parsed.name; 41 | resourcePath = parsed.dir + path.sep; 42 | } 43 | 44 | if (typeof context !== "undefined") { 45 | directory = path 46 | .relative(context, resourcePath + "_") 47 | .replace(/\\/g, "/") 48 | .replace(/\.\.(\/)?/g, "_$1"); 49 | directory = directory.substr(0, directory.length - 1); 50 | } else { 51 | directory = resourcePath.replace(/\\/g, "/").replace(/\.\.(\/)?/g, "_$1"); 52 | } 53 | 54 | if (directory.length <= 1) { 55 | directory = ""; 56 | } else { 57 | // directory.length > 1 58 | folder = path.basename(directory); 59 | } 60 | } 61 | 62 | if (loaderContext.resourceQuery && loaderContext.resourceQuery.length > 1) { 63 | query = loaderContext.resourceQuery; 64 | 65 | const hashIdx = query.indexOf("#"); 66 | 67 | if (hashIdx >= 0) { 68 | query = query.substr(0, hashIdx); 69 | } 70 | } 71 | 72 | let url = filename; 73 | 74 | if (content) { 75 | // Match hash template 76 | url = url 77 | // `hash` and `contenthash` are same in `loader-utils` context 78 | // let's keep `hash` for backward compatibility 79 | .replace( 80 | /\[(?:([^[:\]]+):)?(?:hash|contenthash)(?::([a-z]+\d*(?:safe)?))?(?::(\d+))?\]/gi, 81 | (all, hashType, digestType, maxLength) => 82 | getHashDigest(content, hashType, digestType, parseInt(maxLength, 10)) 83 | ); 84 | } 85 | 86 | url = url 87 | .replace(/\[ext\]/gi, () => ext) 88 | .replace(/\[name\]/gi, () => basename) 89 | .replace(/\[path\]/gi, () => directory) 90 | .replace(/\[folder\]/gi, () => folder) 91 | .replace(/\[query\]/gi, () => query); 92 | 93 | if (regExp && loaderContext.resourcePath) { 94 | const match = loaderContext.resourcePath.match(new RegExp(regExp)); 95 | 96 | match && 97 | match.forEach((matched, i) => { 98 | url = url.replace(new RegExp("\\[" + i + "\\]", "ig"), matched); 99 | }); 100 | } 101 | 102 | if ( 103 | typeof loaderContext.options === "object" && 104 | typeof loaderContext.options.customInterpolateName === "function" 105 | ) { 106 | url = loaderContext.options.customInterpolateName.call( 107 | loaderContext, 108 | url, 109 | name, 110 | options 111 | ); 112 | } 113 | 114 | return url; 115 | } 116 | 117 | module.exports = interpolateName; 118 | -------------------------------------------------------------------------------- /lib/isUrlRequest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | 5 | function isUrlRequest(url) { 6 | // An URL is not an request if 7 | 8 | // 1. Allow `data URI` 9 | if (/^data:/i.test(url)) { 10 | return true; 11 | } 12 | 13 | // 2. It's an absolute url and it is not `windows` path like `C:\dir\file` 14 | if (/^[a-z][a-z0-9+.-]*:/i.test(url) && !path.win32.isAbsolute(url)) { 15 | return false; 16 | } 17 | 18 | // 3. It's a protocol-relative 19 | if (/^\/\//.test(url)) { 20 | return false; 21 | } 22 | 23 | // 4. It's some kind of url for a template 24 | if (/^#/.test(url)) { 25 | return false; 26 | } 27 | 28 | return true; 29 | } 30 | 31 | module.exports = isUrlRequest; 32 | -------------------------------------------------------------------------------- /lib/urlToRequest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // we can't use path.win32.isAbsolute because it also matches paths starting with a forward slash 4 | const matchNativeWin32Path = /^[A-Z]:[/\\]|^\\\\/i; 5 | 6 | function urlToRequest(url, root) { 7 | // Do not rewrite an empty url 8 | if (url === "") { 9 | return ""; 10 | } 11 | 12 | const moduleRequestRegex = /^[^?]*~/; 13 | let request; 14 | 15 | if (matchNativeWin32Path.test(url)) { 16 | // absolute windows path, keep it 17 | request = url; 18 | } else if (root !== undefined && root !== false && /^\//.test(url)) { 19 | // if root is set and the url is root-relative 20 | switch (typeof root) { 21 | // 1. root is a string: root is prefixed to the url 22 | case "string": 23 | // special case: `~` roots convert to module request 24 | if (moduleRequestRegex.test(root)) { 25 | request = root.replace(/([^~/])$/, "$1/") + url.slice(1); 26 | } else { 27 | request = root + url; 28 | } 29 | break; 30 | // 2. root is `true`: absolute paths are allowed 31 | // *nix only, windows-style absolute paths are always allowed as they doesn't start with a `/` 32 | case "boolean": 33 | request = url; 34 | break; 35 | default: 36 | throw new Error( 37 | "Unexpected parameters to loader-utils 'urlToRequest': url = " + 38 | url + 39 | ", root = " + 40 | root + 41 | "." 42 | ); 43 | } 44 | } else if (/^\.\.?\//.test(url)) { 45 | // A relative url stays 46 | request = url; 47 | } else { 48 | // every other url is threaded like a relative url 49 | request = "./" + url; 50 | } 51 | 52 | // A `~` makes the url an module 53 | if (moduleRequestRegex.test(request)) { 54 | request = request.replace(moduleRequestRegex, ""); 55 | } 56 | 57 | return request; 58 | } 59 | 60 | module.exports = urlToRequest; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loader-utils", 3 | "version": "3.3.1", 4 | "author": "Tobias Koppers @sokra", 5 | "description": "utils for webpack loaders", 6 | "dependencies": {}, 7 | "scripts": { 8 | "lint": "prettier --list-different . && eslint .", 9 | "pretest": "yarn lint", 10 | "test": "jest", 11 | "test:only": "jest --coverage", 12 | "test:ci": "yarn test:only", 13 | "release": "yarn test && standard-version" 14 | }, 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/webpack/loader-utils.git" 19 | }, 20 | "engines": { 21 | "node": ">= 12.13.0" 22 | }, 23 | "devDependencies": { 24 | "coveralls": "^3.1.1", 25 | "eslint": "^8.0.1", 26 | "eslint-plugin-node": "^11.1.0", 27 | "jest": "^27.3.1", 28 | "prettier": "^2.4.1", 29 | "standard-version": "^9.3.2" 30 | }, 31 | "main": "lib/index.js", 32 | "files": [ 33 | "lib" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/getHashDigest.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const loaderUtils = require("../"); 4 | 5 | describe("getHashDigest()", () => { 6 | [ 7 | ["test string", "xxhash64", "hex", undefined, "e9e2c351e3c6b198"], 8 | ["test string", "xxhash64", "base64", undefined, "6eLDUePGsZg="], 9 | ["test string", "xxhash64", "base52", undefined, "byfYGDmnmyUr"], 10 | ["abc\\0♥", "xxhash64", "hex", undefined, "4b9a34297dc03d20"], 11 | ["abc\\0💩", "xxhash64", "hex", undefined, "86733ec125b93904"], 12 | ["abc\\0💩", "xxhash64", "base64", undefined, "hnM+wSW5OQQ="], 13 | ["abc\\0♥", "xxhash64", "base64", undefined, "S5o0KX3APSA="], 14 | ["abc\\0💩", "xxhash64", "base52", undefined, "acfByjQcJZIU"], 15 | ["abc\\0♥", "xxhash64", "base52", undefined, "aqdLyAQjLlod"], 16 | 17 | ["test string", "md4", "hex", 4, "2e06"], 18 | ["test string", "md4", "base64", undefined, "Lgbt1PFiMmjFpRcw2KCyrw=="], 19 | ["test string", "md4", "base52", undefined, "egWqIKxsDHdZTteemJqXfuo"], 20 | ["abc\\0♥", "md4", "hex", undefined, "46b9627fecf49b80eaf01c01d86ae9fd"], 21 | ["abc\\0💩", "md4", "hex", undefined, "45aa5b332f8e562aaf0106ad6fc1d78f"], 22 | ["abc\\0💩", "md4", "base64", undefined, "RapbMy+OViqvAQatb8HXjw=="], 23 | ["abc\\0♥", "md4", "base64", undefined, "Rrlif+z0m4Dq8BwB2Grp/Q=="], 24 | ["abc\\0♥", "md4", "base64safe", undefined, "3ZWmHo0hPMWE2rZeN_oHB6"], 25 | ["abc\\0💩", "md4", "base52", undefined, "dtXZENFEkYHXGxOkJbevPoD"], 26 | ["abc\\0♥", "md4", "base52", undefined, "fYFFcfXRGsVweukHKlPayHs"], 27 | 28 | ["test string", "md5", "hex", 4, "6f8d"], 29 | [ 30 | "test string", 31 | "md5", 32 | "hex", 33 | undefined, 34 | "6f8db599de986fab7a21625b7916589c", 35 | ], 36 | ["test string", "md5", "base52", undefined, "dJnldHSAutqUacjgfBQGLQx"], 37 | ["test string", "md5", "base64", undefined, "b421md6Yb6t6IWJbeRZYnA=="], 38 | ["test string", "md5", "base26", undefined, "bhtsgujtzvmjtgtzlqvubqggbvgx"], 39 | ["test string", "md5", "base26", 6, "ggbvgx"], 40 | ["abc\\0♥", "md5", "hex", undefined, "2e897b64f8050e66aff98d38f7a012c5"], 41 | ["abc\\0💩", "md5", "hex", undefined, "63ad5b3d675c5890e0c01ed339ba0187"], 42 | ["abc\\0💩", "md5", "base64", undefined, "Y61bPWdcWJDgwB7TOboBhw=="], 43 | ["abc\\0♥", "md5", "base64", undefined, "Lol7ZPgFDmav+Y0496ASxQ=="], 44 | ["abc\\0💩", "md5", "base52", undefined, "djhVWGHaUKUxqxEhcTnOfBx"], 45 | ["abc\\0♥", "md5", "base52", undefined, "eHeasSeRyOnorzxUJpayzJc"], 46 | 47 | [ 48 | "test string", 49 | "sha512", 50 | "base64", 51 | undefined, 52 | "EObWR69EYkRC84jCwUp4f/ixfmFluD12fsBHdo2MvLcaGjIm58x4Frx5wEJ9lKnaaIxBo5kse/Xk18w+C+XbrA==", 53 | ], 54 | [ 55 | "test string", 56 | "sha512", 57 | "hex", 58 | undefined, 59 | "10e6d647af44624442f388c2c14a787ff8b17e6165b83d767ec047768d8cbcb71a1a3226e7cc7816bc79c0427d94a9da688c41a3992c7bf5e4d7cc3e0be5dbac", 60 | ], 61 | ].forEach((test) => { 62 | it( 63 | "should getHashDigest " + 64 | test[0] + 65 | " " + 66 | test[1] + 67 | " " + 68 | test[2] + 69 | " " + 70 | test[3], 71 | () => { 72 | const hashDigest = loaderUtils.getHashDigest( 73 | test[0], 74 | test[1], 75 | test[2], 76 | test[3] 77 | ); 78 | 79 | expect(hashDigest).toBe(test[4]); 80 | } 81 | ); 82 | }); 83 | }); 84 | 85 | function testDistribution(digestType, length, tableSize, iterations) { 86 | const lowerBound = Math.round(iterations / 2); 87 | const upperBound = Math.round(iterations * 2); 88 | 89 | const stats = []; 90 | for (let i = tableSize * iterations; i-- > 0; ) { 91 | const generatedString = loaderUtils.getHashDigest( 92 | `Some input #${i}`, 93 | undefined, 94 | digestType, 95 | length 96 | ); 97 | 98 | for (let pos = 0; pos < length; pos++) { 99 | const char = generatedString[pos]; 100 | stats[pos] = stats[pos] || {}; 101 | stats[pos][char] = (stats[pos][char] || 0) + 1; 102 | } 103 | } 104 | 105 | for (let pos = 0; pos < length; pos++) { 106 | const chars = Object.keys(stats[pos]).sort(); 107 | test(`distinct chars at position ${pos}`, () => { 108 | expect(chars.length).toBe(tableSize); 109 | }); 110 | for (const char of chars) { 111 | test(`occurences of char "${char}" at position ${pos} should be around ${iterations}`, () => { 112 | expect(stats[pos][char]).toBeLessThanOrEqual(upperBound); 113 | expect(stats[pos][char]).toBeGreaterThanOrEqual(lowerBound); 114 | }); 115 | } 116 | } 117 | } 118 | 119 | describe("getHashDigest() char distribution", () => { 120 | describe("should be uniform for base62", () => { 121 | testDistribution("base62", 8, 62, 100); 122 | }); 123 | describe("should be uniform for base26", () => { 124 | testDistribution("base26", 8, 26, 100); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/interpolateName.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const loaderUtils = require("../"); 4 | 5 | describe("interpolateName()", () => { 6 | function run(tests) { 7 | tests.forEach((test) => { 8 | const args = test[0]; 9 | const expected = test[1]; 10 | const message = test[2]; 11 | it(message, () => { 12 | const result = loaderUtils.interpolateName.apply(loaderUtils, args); 13 | if (typeof expected === "function") { 14 | expected(result); 15 | } else { 16 | expect(result).toBe(expected); 17 | } 18 | }); 19 | }); 20 | } 21 | 22 | [ 23 | [ 24 | "/app/js/javascript.js", 25 | "js/[hash].script.[ext]", 26 | "test content", 27 | "js/0e6882304e9adbd5.script.js", 28 | ], 29 | [ 30 | "/app/js/javascript.js", 31 | "js/[contenthash].script.[ext]", 32 | "test content", 33 | "js/0e6882304e9adbd5.script.js", 34 | ], 35 | [ 36 | "/app/page.html", 37 | "html-[hash:6].html", 38 | "test content", 39 | "html-0e6882.html", 40 | ], 41 | [ 42 | "/app/page.html", 43 | "html-[contenthash:6].html", 44 | "test content", 45 | "html-0e6882.html", 46 | ], 47 | ["/app/flash.txt", "[hash]", "test content", "0e6882304e9adbd5"], 48 | ["/app/flash.txt", "[contenthash]", "test content", "0e6882304e9adbd5"], 49 | [ 50 | "/app/img/image.png", 51 | "[sha512:hash:base64:7].[ext]", 52 | "test content", 53 | "DL9MrvO.png", 54 | ], 55 | [ 56 | "/app/img/image.png", 57 | "[sha512:contenthash:base64:7].[ext]", 58 | "test content", 59 | "DL9MrvO.png", 60 | ], 61 | [ 62 | "/app/dir/file.png", 63 | "[path][name].[ext]?[hash]", 64 | "test content", 65 | "/app/dir/file.png?0e6882304e9adbd5", 66 | ], 67 | [ 68 | "/app/dir/file.png", 69 | "[path][name].[ext]?[contenthash]", 70 | "test content", 71 | "/app/dir/file.png?0e6882304e9adbd5", 72 | ], 73 | [ 74 | "/vendor/test/images/loading.gif", 75 | (path) => path.replace(/\/?vendor\/?/, ""), 76 | "test content", 77 | "test/images/loading.gif", 78 | ], 79 | [ 80 | "/pathWith.period/filename.js", 81 | "js/[name].[ext]", 82 | "test content", 83 | "js/filename.js", 84 | ], 85 | [ 86 | "/pathWith.period/filenameWithoutExt", 87 | "js/[name].[ext]", 88 | "test content", 89 | "js/filenameWithoutExt.bin", 90 | ], 91 | [ 92 | "/lib/components/modal/modal.css", 93 | "[name]__modalTitle___[sha1:hash:hex:4]", 94 | "test content", 95 | "modal__modalTitle___1eeb", 96 | ], 97 | [ 98 | "/lib/components/modal/modal.css", 99 | "[name]__modalTitle___[sha1:contenthash:hex:4]", 100 | "test content", 101 | "modal__modalTitle___1eeb", 102 | ], 103 | [ 104 | "/lib/components/modal/modal.css", 105 | "[name].[md4:hash:base64:20].[ext]", 106 | "test content", 107 | "modal.ppiZgUkxKA4vUnIZrWrH.css", 108 | ], 109 | [ 110 | "/lib/components/modal/modal.css", 111 | "[name].[md5:hash:base64:20].[ext]", 112 | "test content", 113 | "modal.lHP90NiApDwht3eNNIch.css", 114 | ], 115 | [ 116 | "/lib/components/modal/modal.css", 117 | "[name].[md5:contenthash:base64:20].[ext]", 118 | "test content", 119 | "modal.lHP90NiApDwht3eNNIch.css", 120 | ], 121 | [ 122 | "/lib/components/modal/modal.css", 123 | "[name].[md5:contenthash:base64safe:20].[ext]", 124 | "test content", 125 | "modal.8osQznuT8jOAwdzg_nek.css", 126 | ], 127 | // Should not interpret without `hash` or `contenthash` 128 | [ 129 | "/lib/components/modal/modal.css", 130 | "[name].[md5::base64:20].[ext]", 131 | "test content", 132 | "modal.[md5::base64:20].css", 133 | ], 134 | [ 135 | "/app/js/javascript.js?foo=bar", 136 | "js/[hash].script.[ext][query]", 137 | "test content", 138 | "js/0e6882304e9adbd5.script.js?foo=bar", 139 | ], 140 | [ 141 | "/app/js/javascript.js?foo=bar&bar=baz", 142 | "js/[hash].script.[ext][query]", 143 | "test content", 144 | "js/0e6882304e9adbd5.script.js?foo=bar&bar=baz", 145 | ], 146 | [ 147 | "/app/js/javascript.js?foo", 148 | "js/[hash].script.[ext][query]", 149 | "test content", 150 | "js/0e6882304e9adbd5.script.js?foo", 151 | ], 152 | [ 153 | "/app/js/javascript.js?", 154 | "js/[hash].script.[ext][query]", 155 | "test content", 156 | "js/0e6882304e9adbd5.script.js", 157 | ], 158 | [ 159 | "/app/js/javascript.js?a", 160 | "js/[hash].script.[ext][query]", 161 | "test content", 162 | "js/0e6882304e9adbd5.script.js?a", 163 | ], 164 | [ 165 | "/app/js/javascript.js?foo=bar#hash", 166 | "js/[hash].script.[ext][query]", 167 | "test content", 168 | "js/0e6882304e9adbd5.script.js?foo=bar", 169 | ], 170 | [ 171 | "/app/js/javascript.js?foo=bar#hash", 172 | (resourcePath, resourceQuery) => { 173 | expect(resourcePath).toBeDefined(); 174 | expect(resourceQuery).toBeDefined(); 175 | 176 | return "js/[hash].script.[ext][query]"; 177 | }, 178 | "test content", 179 | "js/0e6882304e9adbd5.script.js?foo=bar", 180 | ], 181 | [ 182 | "/app/js/javascript.js?a", 183 | (resourcePath, resourceQuery) => { 184 | expect(resourcePath).toBeDefined(); 185 | expect(resourceQuery).toBeDefined(); 186 | 187 | return "js/[hash].script.[ext][query]"; 188 | }, 189 | "test content", 190 | "js/0e6882304e9adbd5.script.js?a", 191 | ], 192 | [ 193 | "/app/js/javascript.js", 194 | (resourcePath, resourceQuery) => { 195 | expect(resourcePath).toBeDefined(); 196 | expect(resourceQuery).not.toBeDefined(); 197 | 198 | return "js/[hash].script.[ext][query]"; 199 | }, 200 | "test content", 201 | "js/0e6882304e9adbd5.script.js", 202 | ], 203 | [ 204 | "/app/js/javascript.js?", 205 | (resourcePath, resourceQuery) => { 206 | expect(resourcePath).toBeDefined(); 207 | expect(resourceQuery).not.toBeDefined(); 208 | 209 | return "js/[hash].script.[ext][query]"; 210 | }, 211 | "test content", 212 | "js/0e6882304e9adbd5.script.js", 213 | ], 214 | ].forEach((test) => { 215 | it("should interpolate " + test[0] + " " + test[1], () => { 216 | let resourcePath = ""; 217 | let resourceQuery = ""; 218 | 219 | const queryIdx = test[0].indexOf("?"); 220 | 221 | if (queryIdx >= 0) { 222 | resourcePath = test[0].substr(0, queryIdx); 223 | resourceQuery = test[0].substr(queryIdx); 224 | } else { 225 | resourcePath = test[0]; 226 | } 227 | 228 | const interpolatedName = loaderUtils.interpolateName( 229 | { resourcePath, resourceQuery }, 230 | test[1], 231 | { content: test[2] } 232 | ); 233 | 234 | expect(interpolatedName).toBe(test[3]); 235 | }); 236 | }); 237 | 238 | [ 239 | "sha1fakename", 240 | "9dxfakename", 241 | "RSA-SHA256-fakename", 242 | "ecdsa-with-SHA1-fakename", 243 | "tls1.1-sha512-fakename", 244 | ].forEach((hashName) => { 245 | it("should pick hash algorithm by name " + hashName, () => { 246 | expect(() => { 247 | const interpolatedName = loaderUtils.interpolateName( 248 | {}, 249 | "[" + hashName + ":hash:base64:10]", 250 | { content: "a" } 251 | ); 252 | // if for any reason the system we're running on has a hash 253 | // algorithm matching any of our bogus names, at least make sure 254 | // the output is not the unmodified name: 255 | expect(interpolatedName[0]).not.toBe("["); 256 | }).toThrow(/digest method not supported/i); 257 | }); 258 | }); 259 | 260 | run([ 261 | [ 262 | [{}, "", { content: "test string" }], 263 | "e9e2c351e3c6b198.bin", 264 | "should interpolate default tokens", 265 | ], 266 | [ 267 | [{}, "[hash:base64]", { content: "test string" }], 268 | "6eLDUePGsZg=", 269 | "should interpolate [hash] token with options", 270 | ], 271 | [ 272 | [{}, "[unrecognized]", { content: "test string" }], 273 | "[unrecognized]", 274 | "should not interpolate unrecognized token", 275 | ], 276 | ]); 277 | 278 | it("should work without options", () => { 279 | const args = [{}, "foo/bar/[hash]"]; 280 | const result = loaderUtils.interpolateName.apply(loaderUtils, args); 281 | 282 | expect(result).toBe("foo/bar/[hash]"); 283 | }); 284 | 285 | describe("no loader context", () => { 286 | const loaderContext = {}; 287 | run([ 288 | [[loaderContext, "[ext]", {}], "bin", "should interpolate [ext] token"], 289 | [ 290 | [loaderContext, "[name]", {}], 291 | "file", 292 | "should interpolate [name] token", 293 | ], 294 | [[loaderContext, "[path]", {}], "", "should interpolate [path] token"], 295 | [ 296 | [loaderContext, "[folder]", {}], 297 | "", 298 | "should interpolate [folder] token", 299 | ], 300 | ]); 301 | }); 302 | 303 | describe("with loader context", () => { 304 | const loaderContext = { resourcePath: "/path/to/file.exe" }; 305 | run([ 306 | [[loaderContext, "[ext]", {}], "exe", "should interpolate [ext] token"], 307 | [ 308 | [loaderContext, "[name]", {}], 309 | "file", 310 | "should interpolate [name] token", 311 | ], 312 | [ 313 | [loaderContext, "[path]", {}], 314 | "/path/to/", 315 | "should interpolate [path] token", 316 | ], 317 | [ 318 | [loaderContext, "[path]", { context: "/path/" }], 319 | "to/", 320 | "should interpolate [path] token with context", 321 | ], 322 | [ 323 | [loaderContext, "[folder]", {}], 324 | "to", 325 | "should interpolate [folder] token", 326 | ], 327 | [ 328 | [{ resourcePath: "." }, "[folder]", {}], 329 | "", 330 | "should interpolate [folder] token with short resourcePath", 331 | ], 332 | ]); 333 | }); 334 | 335 | run([ 336 | [ 337 | [ 338 | { 339 | resourcePath: "/xyz", 340 | options: { 341 | customInterpolateName(str, name, options) { 342 | return str + "-" + name + "-" + options.special; 343 | }, 344 | }, 345 | }, 346 | "[name]", 347 | { 348 | special: "special", 349 | }, 350 | ], 351 | "xyz-[name]-special", 352 | "should provide a custom interpolateName function in options", 353 | ], 354 | [ 355 | [ 356 | { 357 | resourcePath: "/foo/xyz.png", 358 | }, 359 | "[1]-[name].[ext]", 360 | { 361 | regExp: /\/([a-z0-9]+)\/[a-z0-9]+\.png$/, 362 | }, 363 | ], 364 | "foo-xyz.png", 365 | "should support regExp in options", 366 | ], 367 | ]); 368 | }); 369 | -------------------------------------------------------------------------------- /test/isUrlRequest.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const loaderUtils = require("../"); 4 | 5 | function ExpectedError(regex) { 6 | this.regex = regex; 7 | } 8 | ExpectedError.prototype.matches = function (err) { 9 | return this.regex.test(err.message); 10 | }; 11 | 12 | describe("isUrlRequest()", () => { 13 | [ 14 | // without root 15 | [["//google.com"], false, "should be negative for scheme-agnostic urls"], 16 | [["http://google.com"], false, "should be negative for http urls"], 17 | [["HTTP://google.com"], false, "should be negative for http urls"], 18 | [["https://google.com"], false, "should be negative for https urls"], 19 | [["HTTPS://google.com"], false, "should be negative for https urls"], 20 | 21 | [["chrome-extension://"], false, "should be negative for nonstandard urls"], 22 | [["moz-extension://"], false, "should be negative for nonstandard urls"], 23 | [ 24 | ["ms-browser-extension://"], 25 | false, 26 | "should be negative for nonstandard urls", 27 | ], 28 | [["custom-extension://"], false, "should be negative for nonstandard urls"], 29 | 30 | [["path/to/thing"], true, "should be positive for implicit relative urls"], 31 | [["./img.png"], true, "should be positive for implicit relative urls"], 32 | [["../img.png"], true, "should be positive for implicit relative urls"], 33 | [ 34 | ["./img.png?foo=bar#hash"], 35 | true, 36 | "should be positive for implicit relative urls", 37 | ], 38 | [ 39 | ["./path/to/thing"], 40 | true, 41 | "should be positive for explicit relative urls", 42 | ], 43 | [["~path/to/thing"], true, "should be positive for module urls (with ~)"], 44 | [ 45 | ["some/other/stuff/and/then~path/to/thing"], 46 | true, 47 | "should be positive for module urls with path prefix", 48 | ], 49 | [ 50 | ["./some/other/stuff/and/then~path/to/thing"], 51 | true, 52 | "should be positive for module urls with relative path prefix", 53 | ], 54 | [["C:/thing"], true, "should be positive for linux path with driver"], 55 | [["C:\\thing"], true, "should be positive for windows path with driver"], 56 | [ 57 | ["directory/things"], 58 | true, 59 | "should be positive for relative path (linux)", 60 | ], 61 | [ 62 | ["directory\\things"], 63 | true, 64 | "should be positive for relative path (windows)", 65 | ], 66 | 67 | // with root (normal path) 68 | [ 69 | ["path/to/thing", "root/dir"], 70 | true, 71 | "should be positive with root if implicit relative url", 72 | ], 73 | [ 74 | ["./path/to/thing", "root/dir"], 75 | true, 76 | "should be positive with root if explicit relative url", 77 | ], 78 | 79 | // with root (boolean) on Windows 80 | [ 81 | ["C:\\path\\to\\thing"], 82 | true, 83 | "should be positive for Windows absolute paths with drive letter", 84 | ], 85 | [ 86 | ["\\\\?\\UNC\\ComputerName\\path\\to\\thing"], 87 | true, 88 | "should be positive for Windows absolute UNC paths", 89 | ], 90 | 91 | // with root (module) 92 | [ 93 | ["/path/to/thing", "~"], 94 | true, 95 | "should be positive for module url if root = ~", 96 | ], 97 | 98 | // with root (module path) 99 | [ 100 | ["/path/to/thing", "~module"], 101 | true, 102 | "should be positive for module prefixes when root starts with ~", 103 | ], 104 | [ 105 | ["/path/to/thing", "~module/"], 106 | true, 107 | "should be positive for module prefixes (with trailing slash) when root starts with ~", 108 | ], 109 | 110 | // error cases 111 | [ 112 | ["/path/to/thing", 1], 113 | new ExpectedError(/unexpected parameters/i), 114 | "should throw an error on invalid root", 115 | ], 116 | 117 | // empty url 118 | [[""], true, "should be positive if url is empty"], 119 | 120 | // about url 121 | [["about:blank"], false, "should be negative for about:blank"], 122 | 123 | // hash 124 | [["#gradient"], false, "should be negative for hash url"], 125 | 126 | // url 127 | [["//sindresorhus.com"], false, "should ignore noscheme url"], 128 | [ 129 | ["//at.alicdn.com/t/font_515771_emcns5054x3whfr.eot"], 130 | false, 131 | "should ignore noscheme url with path", 132 | ], 133 | [ 134 | ["https://example.com/././foo"], 135 | false, 136 | "should ignore absolute url with relative", 137 | ], 138 | 139 | // non standard protocols 140 | [ 141 | ["file://sindresorhus.com"], 142 | false, 143 | "should ignore non standard protocols (file)", 144 | ], 145 | [ 146 | ["mailto:someone@example.com"], 147 | false, 148 | "should ignore non standard protocols (mailto)", 149 | ], 150 | [ 151 | ["data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D"], 152 | true, 153 | "should work with non standard protocols (data)", 154 | ], 155 | [ 156 | ["DATA:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D"], 157 | true, 158 | "should work with non standard protocols (data)", 159 | ], 160 | 161 | // root-relative url 162 | [["/"], true, "should work with root-relative url 1"], 163 | [["//"], false, "ignore root-relative url 1"], 164 | ].forEach((test) => { 165 | it(test[2], () => { 166 | const expected = test[1]; 167 | 168 | try { 169 | const request = loaderUtils.isUrlRequest.apply(loaderUtils, test[0]); 170 | 171 | expect(request).toBe(expected); 172 | } catch (e) { 173 | if (expected instanceof ExpectedError) { 174 | expect(expected.matches(e)).toBe(true); 175 | } else { 176 | throw new Error("should not have thrown an error: " + e.message); 177 | } 178 | } 179 | }); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /test/urlToRequest.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const loaderUtils = require("../"); 4 | 5 | function ExpectedError(regex) { 6 | this.regex = regex; 7 | } 8 | 9 | ExpectedError.prototype.matches = function (err) { 10 | return this.regex.test(err.message); 11 | }; 12 | 13 | describe("urlToRequest()", () => { 14 | [ 15 | // without root 16 | [ 17 | ["path/to/thing"], 18 | "./path/to/thing", 19 | "should handle implicit relative urls", 20 | ], 21 | [ 22 | ["./path/to/thing"], 23 | "./path/to/thing", 24 | "should handle explicit relative urls", 25 | ], 26 | [["~path/to/thing"], "path/to/thing", "should handle module urls (with ~)"], 27 | [ 28 | ["some/other/stuff/and/then~path/to/thing"], 29 | "path/to/thing", 30 | "should handle module urls with path prefix", 31 | ], 32 | [ 33 | ["./some/other/stuff/and/then~path/to/thing"], 34 | "path/to/thing", 35 | "should handle module urls with relative path prefix", 36 | ], 37 | // with root (normal path) 38 | [ 39 | ["path/to/thing", "root/dir"], 40 | "./path/to/thing", 41 | "should do nothing with root if implicit relative url", 42 | ], 43 | [ 44 | ["./path/to/thing", "root/dir"], 45 | "./path/to/thing", 46 | "should do nothing with root if explicit relative url", 47 | ], 48 | [ 49 | ["/path/to/thing", "root/dir"], 50 | "root/dir/path/to/thing", 51 | "should include root if root-relative url", 52 | ], 53 | // with root (boolean) 54 | [ 55 | ["/path/to/thing", true], 56 | "/path/to/thing", 57 | "should allow root-relative to exist as-is if root = `true`", 58 | ], 59 | // with root (boolean) on Windows 60 | [ 61 | ["C:\\path\\to\\thing"], 62 | "C:\\path\\to\\thing", 63 | "should handle Windows absolute paths with drive letter", 64 | ], 65 | [ 66 | ["\\\\?\\UNC\\ComputerName\\path\\to\\thing"], 67 | "\\\\?\\UNC\\ComputerName\\path\\to\\thing", 68 | "should handle Windows absolute UNC paths", 69 | ], 70 | // with root (module) 71 | [ 72 | ["/path/to/thing", "~"], 73 | "path/to/thing", 74 | "should convert to module url if root = ~", 75 | ], 76 | // with root (module path) 77 | [ 78 | ["/path/to/thing", "~module"], 79 | "module/path/to/thing", 80 | "should allow module prefixes when root starts with ~", 81 | ], 82 | [ 83 | ["/path/to/thing", "~module/"], 84 | "module/path/to/thing", 85 | "should allow module prefixes (with trailing slash) when root starts with ~", 86 | ], 87 | // error cases 88 | [ 89 | ["/path/to/thing", 1], 90 | new ExpectedError(/unexpected parameters/i), 91 | "should throw an error on invalid root", 92 | ], 93 | // difficult cases 94 | [ 95 | ["a:b-not-\\window-path"], 96 | "./a:b-not-\\window-path", 97 | "should not incorrectly detect windows paths", 98 | ], 99 | // empty url 100 | [[""], "", "should do nothing if url is empty"], 101 | ].forEach((test) => { 102 | it(test[2], () => { 103 | const expected = test[1]; 104 | 105 | try { 106 | const request = loaderUtils.urlToRequest.apply(loaderUtils, test[0]); 107 | 108 | expect(request).toBe(expected); 109 | } catch (e) { 110 | if (expected instanceof ExpectedError) { 111 | expect(expected.matches(e)).toBe(true); 112 | } else { 113 | throw new Error("should not have thrown an error: " + e.message); 114 | } 115 | } 116 | }); 117 | }); 118 | }); 119 | --------------------------------------------------------------------------------