├── .gitignore ├── .nvmrc ├── .prettierrc ├── DEVELOPERS.md ├── Dockerfile ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── icons.css │ ├── icons.png │ ├── icons@2x.png │ ├── main.js │ ├── search.js │ ├── style.css │ ├── widgets.png │ └── widgets@2x.png ├── classes │ └── SquooshPlugin.html ├── enums │ ├── AVIFTune.html │ ├── Csp.html │ ├── MozJpegColorSpace.html │ └── UVMode.html ├── index.html ├── interfaces │ ├── AvifEncodeOptions.html │ ├── Extension.html │ ├── HookContext.html │ ├── InitializeOptions.html │ ├── JxlEncodeOptions.html │ ├── MozJPEGEncodeOptions.html │ ├── OxiPngEncodeOptions.html │ ├── PrepareOptions.html │ ├── RequestOptions.html │ ├── WP2EncodeOptions.html │ └── WebPEncodeOptions.html └── modules.html ├── jest.config.js ├── logo ├── favicons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ └── site.webmanifest ├── logo.kra └── logo.png ├── package-lock.json ├── package.json ├── scripts └── prepare-publish.ts ├── src ├── __test__ │ ├── basic │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── src │ │ │ ├── index.js │ │ │ └── logo.png │ └── util │ │ ├── checksum.ts │ │ ├── createWebpackConfig.ts │ │ ├── getDistDir.ts │ │ ├── image-loader.js │ │ ├── removeDistFolder.ts │ │ └── runWebpackBuild.ts ├── extensions │ ├── BaseResolverExtension │ │ ├── BaseResolverExtension.ts │ │ └── index.ts │ ├── BasicCacheExtension │ │ ├── BasicCacheExtension.ts │ │ └── index.ts │ ├── DefaultOptionsExtension │ │ ├── DefaultOptionsExtension.ts │ │ ├── default-options.ts │ │ └── index.ts │ ├── DefaultOutputPathExtension │ │ ├── DefaultOutputPathExtension.ts │ │ └── index.ts │ ├── EnsureOutputDirectoryExtension │ │ ├── EnsureOutputDirectoryExtension.ts │ │ └── index.ts │ ├── default.extensions.ts │ ├── extension-error.ts │ └── index.ts ├── global.d.ts ├── index.ts ├── tokens │ └── index.ts ├── types │ ├── encoder-options.ts │ ├── extensions │ │ ├── extension.ts │ │ ├── hooks.ts │ │ └── index.ts │ ├── index.ts │ ├── plugin-options.ts │ ├── squoosh.ts │ └── webpack.ts ├── utils │ ├── checksum.ts │ ├── exists.ts │ ├── extensions.ts │ ├── getWebpackVersion.ts │ ├── match.ts │ ├── replaceExtension.ts │ └── sort.ts └── worker │ ├── events.ts │ ├── index.ts │ └── types.ts ├── tsconfig.json ├── tsconfig.scripts.json └── typedoc.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | scripts/dist 4 | npm 5 | logo/logo.kra~ 6 | logo/logo.png~ 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.14.2 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /DEVELOPERS.md: -------------------------------------------------------------------------------- 1 | # Documentation for Developers 2 | 3 | ## Publishing to NPM 4 | 5 | Run the run-publish script: 6 | 7 | `npm run run-publish` 8 | 9 | ## Publishing to Digital Ocean 10 | 11 | Before you begin, you will need to install and configure `doctl`. A guide can be found [here](https://docs.digitalocean.com/reference/doctl/how-to/install/). 12 | 13 | Switch to the authentication context you set up in the above guide: 14 | 15 | ``` 16 | doctl auth list 17 | doctl auth switch --context 18 | ``` 19 | 20 | Authenticate Docker with the container registry: 21 | 22 | `doctl registry login` 23 | 24 | Build the docker image: 25 | 26 | `docker build -t squoosh-webpack-plugin-documentation .` 27 | 28 | Optionally, test the docker image by running the following command and going to [http://localhost:8080/](http://localhost:8080/): 29 | 30 | `docker run -d -p 8080:8080 squoosh-webpack-plugin-documentation` 31 | 32 | Tag the image: 33 | 34 | `docker tag squoosh-webpack-plugin-documentation registry.digitalocean.com/bcheidemann/squoosh-webpack-plugin-documentation` 35 | 36 | Push the image to the container registry: 37 | 38 | `docker push registry.digitalocean.com/bcheidemann/squoosh-webpack-plugin-documentation` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM halverneus/static-file-server:latest 2 | ADD docs /web 3 | ADD logo/favicons /web 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Squoosh Webpack Plugin Logo](https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/master/logo/favicons/android-chrome-192x192.png) 2 | 3 | # Squoosh Webpack Plugin 4 | 5 | The squoosh webpack plugin allows you to integrate [Squoosh](https://squoosh.app/) into your webpack builds. Squoosh is an open source `"image compression web app that reduces image sizes through numerous formats"` ([Squoosh Github](https://github.com/GoogleChromeLabs/squoosh)) as well as a javascript library and command line tool. 6 | 7 | ## Typescript 8 | 9 | This project is written in typescript, meaning that in most modern IDEs you will receive type hinting - even when you're in a javascript file such as `webpack.config.js`. 10 | 11 | ## What can it do? 12 | 13 | The squoosh webpack plugin allows you to leverage the power of squoosh in your webpack builds. It will automatically optimise any images you import in your javascript web app. 14 | 15 | By default, it generates a unique ID for each image so as not to generate duplicates. This also means that it will only generate new images when you change the encoder options, update an image (when not using the `preserveFileName` option), or add/rename an image. 16 | 17 | It supports all codecs and encoder options supported by squoosh and allows you to configure these in the same way you would when using the squoosh command line tool or API directly. 18 | 19 | ## What can't it do? 20 | 21 | The squoosh webpack plugin currently does not currently support any preprocessor options such as image resizing or rotation. 22 | 23 | ## What next? 24 | 25 | Planned features for this plugin are as follows: 26 | 27 | - Add support for all preprocessor options available in Squoosh. 28 | - Add support for per image config files so that you can customise the encoder and preprocessor options an a per image basis 29 | - Any suggestions for improvements? Please raise an issue on [github](https://github.com/bcheidemann/squoosh-webpack-plugin) - I'd love to hear from you! 30 | 31 | ## Documentation 32 | 33 | Documentation for this project is available [here](https://squoosh-webpack-plugin.info/). 34 | 35 | ## Basic Usage 36 | 37 | First install the plugin from yarn or npm. 38 | 39 | ``` 40 | npm install --save-dev squoosh-webpack-plugin 41 | ``` 42 | 43 | Now add the following to your `webpack.config.js`. 44 | 45 | ```javascript 46 | const { SquooshPlugin } = require("squoosh-webpack-plugin"); 47 | 48 | module.exports = (config, context) => { 49 | return { 50 | ...config, 51 | plugins: [ 52 | ...config.plugins, 53 | new SquooshPlugin(), 54 | ], 55 | }; 56 | }; 57 | ``` 58 | 59 | This will initialise the plugin with default options. By default, squoosh will use the `mozjpeg` codec. By default, output images will be placed in the same folder as the input file. 60 | 61 | ## Basic Configuration 62 | 63 | The plugin constructor accepts an options object which allows you to configure the default behaviour. 64 | 65 | ```javascript 66 | new SquooshPlugin({ 67 | ...options, 68 | }); 69 | ``` 70 | 71 | One of the first things you might want to change is the output directory. If you have a specific directory to which you wish to export your images after Squoosh has done it's magic, this can be specified with the `outDir` option. 72 | 73 | ```javascript 74 | new SquooshPlugin({ 75 | ...options, 76 | outDir: 'public/images', 77 | }); 78 | ``` 79 | 80 | You can choose a codec and/or specify some encoder options as shown below. 81 | 82 | ```javascript 83 | new SquooshPlugin({ 84 | ...options, 85 | codec: 'mozjpeg', 86 | encoderOptions: { 87 | quality: 65, 88 | }, 89 | }); 90 | ``` 91 | 92 | ## Options 93 | 94 | | Option | Type | Default | Description | 95 | | - | - | - | - | 96 | | extensions | Aray \| Function | See [extensions](#extensions) | Extensions can be used to implement arbitrary custom behaviour. | 97 | | encoderOptions | `object` | - | The encoder options passed to Squoosh. Each codec has different default options. | 98 | | codec | `string` | `"mozjpeg"` | Codec used to encode images. | 99 | | useWorker | `boolean` | `true` | Invoke Squoosh from a Node child process. This is not a performance optimisation but disabling it may cause conflicts with certain other Webpack plugins. | 100 | | | | | | 101 | | requestPrefix | `string` | - | If specified, only files starting with this prefix will be included. This option is managed internally by the `BaseResolverExtension`. | 102 | | include | `RegExp` | `/\.(jpeg\|jpg\|png)$/` | If defined, files which match the regex pattern will be included. This option is managed internally by the `BaseResolverExtension`. | 103 | | exclude | `RegExp` | - | If defined, files which match the regex pattern will be excluded. This option is managed internally by the `BaseResolverExtension`. | 104 | | dirs | `Array` | - | If defined, only files in one of the directories will be included. This option is managed internally by the `BaseResolverExtension`. | 105 | | | | | | 106 | | outDir | `string` | - | If defined, encoded images will be output to this directory. This option is managed internally by the `DefaultOutputPathExtension`. | 107 | | uuidNamespace | `string` | - | Used to produce a unique filename if `preserveFileName` is set to `false`. This option is managed internally by the `DefaultOutputPathExtension`. | 108 | | preserveFileName | `boolean` | `false` | If true, file names will be preserved and only the extension will change. This option is managed internally by the `DefaultOutputPathExtension`. | 109 | | | | | | 110 | 111 | 112 | For more information on options, see the [documentation](https://squoosh-webpack-plugin.info/) and [Squoosh github page](https://github.com/GoogleChromeLabs/squoosh). 113 | 114 | ## Extensions 115 | 116 | Extensions can be used to customise the behaviour of the plugin. They are used internally to implement the default behaviour. 117 | 118 | Setting the `extensions` option to an array will concatenate the extensions in the array with the default extensions: 119 | 120 | - `BaseResolverExtension` 121 | - `BasicCacheExtension` 122 | - `DefaultOptionsExtension` 123 | - `DefaultOutputPathExtension` 124 | - `EnsureOutputDirectoryExtension` 125 | 126 | It is also possible to set the `extensions` option to a function. This will receive an array of the default extensions and should return an array of extensions. This is primarily useful if you wish to exclude one or more of the default extensions. 127 | 128 | Extensions should be an object or class which implements the `Extension` type. It is recommended to define the name property for error logging purposes. One or more hook functions may be defined to tap into various stages of the plugins lifecycle. 129 | 130 | The available hooks are: 131 | 132 | - `initialize` 133 | - `request` 134 | - `prepare` 135 | 136 | If you intend to implement an extension, it is recommended to use the existing internal plugins for guidance. If you feel the functionality you are implementing should be part of the plugin, feel free to open a PR or an issue. 137 | 138 | ## Contributing 139 | 140 | Want to get involved? Great! Feel free to help out by raising a bug, submitting a feature request or opening a pull request in [github](https://github.com/bcheidemann/squoosh-webpack-plugin). 141 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {presets: ['@babel/preset-env']} 2 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #001080; 3 | --dark-hl-0: #9CDCFE; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #0000FF; 7 | --dark-hl-2: #569CD6; 8 | --light-hl-3: #0070C1; 9 | --dark-hl-3: #4FC1FF; 10 | --light-hl-4: #795E26; 11 | --dark-hl-4: #DCDCAA; 12 | --light-hl-5: #A31515; 13 | --dark-hl-5: #CE9178; 14 | --light-hl-6: #267F99; 15 | --dark-hl-6: #4EC9B0; 16 | --light-hl-7: #AF00DB; 17 | --dark-hl-7: #C586C0; 18 | --light-hl-8: #098658; 19 | --dark-hl-8: #B5CEA8; 20 | --light-code-background: #F5F5F5; 21 | --dark-code-background: #1E1E1E; 22 | } 23 | 24 | @media (prefers-color-scheme: light) { :root { 25 | --hl-0: var(--light-hl-0); 26 | --hl-1: var(--light-hl-1); 27 | --hl-2: var(--light-hl-2); 28 | --hl-3: var(--light-hl-3); 29 | --hl-4: var(--light-hl-4); 30 | --hl-5: var(--light-hl-5); 31 | --hl-6: var(--light-hl-6); 32 | --hl-7: var(--light-hl-7); 33 | --hl-8: var(--light-hl-8); 34 | --code-background: var(--light-code-background); 35 | } } 36 | 37 | @media (prefers-color-scheme: dark) { :root { 38 | --hl-0: var(--dark-hl-0); 39 | --hl-1: var(--dark-hl-1); 40 | --hl-2: var(--dark-hl-2); 41 | --hl-3: var(--dark-hl-3); 42 | --hl-4: var(--dark-hl-4); 43 | --hl-5: var(--dark-hl-5); 44 | --hl-6: var(--dark-hl-6); 45 | --hl-7: var(--dark-hl-7); 46 | --hl-8: var(--dark-hl-8); 47 | --code-background: var(--dark-code-background); 48 | } } 49 | 50 | body.light { 51 | --hl-0: var(--light-hl-0); 52 | --hl-1: var(--light-hl-1); 53 | --hl-2: var(--light-hl-2); 54 | --hl-3: var(--light-hl-3); 55 | --hl-4: var(--light-hl-4); 56 | --hl-5: var(--light-hl-5); 57 | --hl-6: var(--light-hl-6); 58 | --hl-7: var(--light-hl-7); 59 | --hl-8: var(--light-hl-8); 60 | --code-background: var(--light-code-background); 61 | } 62 | 63 | body.dark { 64 | --hl-0: var(--dark-hl-0); 65 | --hl-1: var(--dark-hl-1); 66 | --hl-2: var(--dark-hl-2); 67 | --hl-3: var(--dark-hl-3); 68 | --hl-4: var(--dark-hl-4); 69 | --hl-5: var(--dark-hl-5); 70 | --hl-6: var(--dark-hl-6); 71 | --hl-7: var(--dark-hl-7); 72 | --hl-8: var(--dark-hl-8); 73 | --code-background: var(--dark-code-background); 74 | } 75 | 76 | .hl-0 { color: var(--hl-0); } 77 | .hl-1 { color: var(--hl-1); } 78 | .hl-2 { color: var(--hl-2); } 79 | .hl-3 { color: var(--hl-3); } 80 | .hl-4 { color: var(--hl-4); } 81 | .hl-5 { color: var(--hl-5); } 82 | .hl-6 { color: var(--hl-6); } 83 | .hl-7 { color: var(--hl-7); } 84 | .hl-8 { color: var(--hl-8); } 85 | pre, code { background: var(--code-background); } 86 | -------------------------------------------------------------------------------- /docs/assets/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/docs/assets/icons.png -------------------------------------------------------------------------------- /docs/assets/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/docs/assets/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/docs/assets/widgets.png -------------------------------------------------------------------------------- /docs/assets/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/docs/assets/widgets@2x.png -------------------------------------------------------------------------------- /docs/enums/AVIFTune.html: -------------------------------------------------------------------------------- 1 | AVIFTune | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

Enumeration AVIFTune

Index

Enumeration members

Enumeration members

auto = 0
psnr = 1
ssim = 2

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/enums/Csp.html: -------------------------------------------------------------------------------- 1 | Csp | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Enumeration members

Enumeration members

kCustom = 2
kYCbCr = 1
kYCoCg = 0
kYIQ = 3

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/enums/MozJpegColorSpace.html: -------------------------------------------------------------------------------- 1 | MozJpegColorSpace | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

Enumeration MozJpegColorSpace

Index

Enumeration members

Enumeration members

GRAYSCALE = 1
RGB = 2
YCbCr = 3

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/enums/UVMode.html: -------------------------------------------------------------------------------- 1 | UVMode | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Enumeration members

UVMode420 = 1
UVMode444 = 2
UVModeAdapt = 0
UVModeAuto = 3

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

squoosh-webpack-plugin

Squoosh Webpack Plugin Logo

2 | 3 | 4 |

Squoosh Webpack Plugin

5 |
6 |

The squoosh webpack plugin allows you to integrate Squoosh into your webpack builds. Squoosh is an open source "image compression web app that reduces image sizes through numerous formats" (Squoosh Github) as well as a javascript library and command line tool.

7 | 8 | 9 |

Typescript

10 |
11 |

This project is written in typescript, meaning that in most modern IDEs you will receive type hinting - even when you're in a javascript file such as webpack.config.js.

12 | 13 | 14 |

What can it do?

15 |
16 |

The squoosh webpack plugin allows you to leverage the power of squoosh in your webpack builds. It will automatically optimise any images you import in your javascript web app.

17 |

By default, it generates a unique ID for each image so as not to generate duplicates. This also means that it will only generate new images when you change the encoder options, update an image (when not using the preserveFileName option), or add/rename an image.

18 |

It supports all codecs and encoder options supported by squoosh and allows you to configure these in the same way you would when using the squoosh command line tool or API directly.

19 | 20 | 21 |

What can't it do?

22 |
23 |

The squoosh webpack plugin currently does not currently support any preprocessor options such as image resizing or rotation.

24 | 25 | 26 |

What next?

27 |
28 |

Planned features for this plugin are as follows:

29 |
    30 |
  • Add support for all preprocessor options available in Squoosh.
  • 31 |
  • Add support for per image config files so that you can customise the encoder and preprocessor options an a per image basis
  • 32 |
  • Any suggestions for improvements? Please raise an issue on github - I'd love to hear from you!
  • 33 |
34 | 35 | 36 |

Documentation

37 |
38 |

Documentation for this project is available here.

39 | 40 | 41 |

Basic Usage

42 |
43 |

First install the plugin from yarn or npm.

44 |
npm install --save-dev squoosh-webpack-plugin
 45 | 
46 |

Now add the following to your webpack.config.js.

47 |
const { SquooshPlugin } = require("squoosh-webpack-plugin");

module.exports = (config, context) => {
return {
...config,
plugins: [
...config.plugins,
new SquooshPlugin(),
],
};
}; 48 |
49 |

This will initialise the plugin with default options. By default, squoosh will use the mozjpeg codec. By default, output images will be placed in the same folder as the input file.

50 | 51 | 52 |

Basic Configuration

53 |
54 |

The plugin constructor accepts an options object which allows you to configure the default behaviour.

55 |
new SquooshPlugin({
...options,
}); 56 |
57 |

One of the first things you might want to change is the output directory. If you have a specific directory to which you wish to export your images after Squoosh has done it's magic, this can be specified with the outDir option.

58 |
new SquooshPlugin({
...options,
outDir: 'public/images',
}); 59 |
60 |

You can choose a codec and/or specify some encoder options as shown below.

61 |
new SquooshPlugin({
...options,
codec: 'mozjpeg',
encoderOptions: {
quality: 65,
},
}); 62 |
63 | 64 | 65 |

Options

66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |
OptionTypeDefaultDescription
extensionsAray | FunctionSee extensionsExtensions can be used to implement arbitrary custom behaviour.
encoderOptionsobject-The encoder options passed to Squoosh. Each codec has different default options.
codecstring"mozjpeg"Codec used to encode images.
useWorkerbooleantrueInvoke Squoosh from a Node child process. This is not a performance optimisation but disabling it may cause conflicts with certain other Webpack plugins.
requestPrefixstring-If specified, only files starting with this prefix will be included. This option is managed internally by the BaseResolverExtension.
includeRegExp/\.(jpeg|jpg|png)$/If defined, files which match the regex pattern will be included. This option is managed internally by the BaseResolverExtension.
excludeRegExp-If defined, files which match the regex pattern will be excluded. This option is managed internally by the BaseResolverExtension.
dirsArray-If defined, only files in one of the directories will be included. This option is managed internally by the BaseResolverExtension.
outDirstring-If defined, encoded images will be output to this directory. This option is managed internally by the DefaultOutputPathExtension.
uuidNamespacestring-Used to produce a unique filename if preserveFileName is set to false. This option is managed internally by the DefaultOutputPathExtension.
preserveFileNamebooleanfalseIf true, file names will be preserved and only the extension will change. This option is managed internally by the DefaultOutputPathExtension.
161 |

For more information on options, see the documentation and Squoosh github page.

162 | 163 | 164 |

Extensions

165 |
166 |

Extensions can be used to customise the behaviour of the plugin. They are used internally to implement the default behaviour.

167 |

Setting the extensions option to an array will concatenate the extensions in the array with the default extensions:

168 |
    169 |
  • BaseResolverExtension
  • 170 |
  • BasicCacheExtension
  • 171 |
  • DefaultOptionsExtension
  • 172 |
  • DefaultOutputPathExtension
  • 173 |
  • EnsureOutputDirectoryExtension
  • 174 |
175 |

It is also possible to set the extensions option to a function. This will receive an array of the default extensions and should return an array of extensions. This is primarily useful if you wish to exclude one or more of the default extensions.

176 |

Extensions should be an object or class which implements the Extension type. It is recommended to define the name property for error logging purposes. One or more hook functions may be defined to tap into various stages of the plugins lifecycle.

177 |

The available hooks are:

178 |
    179 |
  • initialize
  • 180 |
  • request
  • 181 |
  • prepare
  • 182 |
183 |

If you intend to implement an extension, it is recommended to use the existing internal plugins for guidance. If you feel the functionality you are implementing should be part of the plugin, feel free to open a PR or an issue.

184 | 185 | 186 |

Contributing

187 |
188 |

Want to get involved? Great! Feel free to help out by raising a bug, submitting a feature request or opening a pull request in github.

189 |

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/AvifEncodeOptions.html: -------------------------------------------------------------------------------- 1 | AvifEncodeOptions | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface AvifEncodeOptions

Hierarchy

  • AvifEncodeOptions

Index

Properties

chromaDeltaQ: boolean
cqAlphaLevel: number
cqLevel: number
denoiseLevel: number
sharpness: number
speed: number
subsample: number
tileColsLog2: number
tileRowsLog2: number
tune: AVIFTune

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/Extension.html: -------------------------------------------------------------------------------- 1 | Extension | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface Extension<Codec>

Type parameters

Hierarchy

  • Extension

Index

Properties

initialize?: InitializeHook<Codec>
name?: string
order?: number
prepare?: PrepareHook<Codec>
request?: RequestHook<Codec>

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/HookContext.html: -------------------------------------------------------------------------------- 1 | HookContext | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface HookContext<Codec>

Type parameters

Hierarchy

  • HookContext

Index

Properties

Properties

options: SquooshPluginOptions<Codec>

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/JxlEncodeOptions.html: -------------------------------------------------------------------------------- 1 | JxlEncodeOptions | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface JxlEncodeOptions

Hierarchy

  • JxlEncodeOptions

Index

Properties

decodingSpeedTier: number
effort: number
epf: number
lossyPalette: boolean
photonNoiseIso: number
progressive: boolean
quality: number

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/OxiPngEncodeOptions.html: -------------------------------------------------------------------------------- 1 | OxiPngEncodeOptions | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface OxiPngEncodeOptions

Hierarchy

  • OxiPngEncodeOptions

Index

Properties

Properties

level: number

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/PrepareOptions.html: -------------------------------------------------------------------------------- 1 | PrepareOptions | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface PrepareOptions<Codec>

Type parameters

Hierarchy

  • PrepareOptions

Index

Properties

codec: Codec
encoderOptions: SquooshEncodeOptions<Codec>
inputPath: string
outputPath?: string
skip: boolean

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/RequestOptions.html: -------------------------------------------------------------------------------- 1 | RequestOptions | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface RequestOptions

Hierarchy

  • RequestOptions

Index

Properties

context: string
include: boolean
request: string

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/WP2EncodeOptions.html: -------------------------------------------------------------------------------- 1 | WP2EncodeOptions | squoosh-webpack-plugin
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface WP2EncodeOptions

Hierarchy

  • WP2EncodeOptions

Index

Properties

alpha_quality: number
csp_type: Csp
effort: number
error_diffusion: number
pass: number
quality: number
sns: number
use_random_matrix: boolean
uv_mode: UVMode

Generated using TypeDoc

-------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | transform: { 4 | '^.+\\.(ts|tsx)?$': 'ts-jest', 5 | "^.+\\.(js|jsx)$": "babel-jest", 6 | }, 7 | roots: [ 8 | "./src/__test__/", 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /logo/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/logo/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /logo/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/logo/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /logo/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/logo/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /logo/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/logo/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /logo/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/logo/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /logo/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/logo/favicons/favicon.ico -------------------------------------------------------------------------------- /logo/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /logo/logo.kra: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/logo/logo.kra -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/logo/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "squoosh-webpack-plugin", 3 | "version": "2.1.1", 4 | "description": "A webpack plugin for squoosh", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "build:docs": "typedoc", 9 | "prepare-publish": "ts-node ./scripts/prepare-publish.ts", 10 | "run-publish": "npm run prepare-publish && npm publish ./npm --access public", 11 | "test": "jest", 12 | "test5": "jest" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/bcheidemann/squoosh-webpack-plugin.git" 17 | }, 18 | "keywords": [ 19 | "squoosh", 20 | "webpack", 21 | "plugin", 22 | "image", 23 | "compression" 24 | ], 25 | "author": "Ben Heidemann", 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/bcheidemann/squoosh-webpack-plugin/issues" 29 | }, 30 | "homepage": "https://squoosh-webpack-plugin.info/", 31 | "peerDependencies": { 32 | "webpack": "4.x || 5.x" 33 | }, 34 | "devDependencies": { 35 | "@babel/preset-env": "^7.16.11", 36 | "@types/checksum": "^0.1.33", 37 | "@types/fs-extra": "^9.0.13", 38 | "@types/jest": "^27.4.1", 39 | "@types/node": "^16.10.3", 40 | "@types/uuid": "^8.3.1", 41 | "@types/webpack4": "npm:@types/webpack@^4.41.26", 42 | "@types/webpack5": "npm:@types/webpack@^5.28.0", 43 | "babel-jest": "^27.5.1", 44 | "jest": "^27.5.1", 45 | "ts-jest": "^27.1.4", 46 | "ts-node": "^10.3.0", 47 | "typedoc": "^0.22.5", 48 | "typescript": "^4.4.3", 49 | "webpack4": "npm:webpack@^4.46.0", 50 | "webpack5": "npm:webpack@^5.72.0" 51 | }, 52 | "dependencies": { 53 | "@squoosh/lib": "^0.4.0", 54 | "fs-extra": "^10.0.0", 55 | "uuid": "^8.3.2", 56 | "checksum": "^1.0.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/prepare-publish.ts: -------------------------------------------------------------------------------- 1 | import { copySync, mkdirSync, removeSync } from 'fs-extra'; 2 | import { join } from 'path'; 3 | import { execSync } from 'child_process'; 4 | 5 | // Get directory paths 6 | const pwd = process.cwd(); 7 | const npmd = join(pwd, 'npm'); 8 | 9 | // Run the build 10 | execSync('npm run build'); 11 | 12 | // Delete all files in npm directory 13 | removeSync(npmd); 14 | 15 | // Create empty npm directory 16 | mkdirSync(npmd); 17 | 18 | // Copy files into npm directory 19 | copySync(join(pwd, 'dist'), join(npmd, 'dist'), { recursive: true }); 20 | copySync(join(pwd, 'package.json'), join(npmd, 'package.json')); 21 | copySync(join(pwd, 'README.md'), join(npmd, 'README.md')); 22 | copySync(join(pwd, 'LICENSE'), join(npmd, 'LICENSE')); 23 | -------------------------------------------------------------------------------- /src/__test__/basic/__snapshots__/basic.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Should run a webpack build with default options Webpack v4 1`] = `"f97721ce8640cb7a7086b565edd955a4c97de7c9"`; 4 | 5 | exports[`Should run a webpack build with default options Webpack v4 2`] = `"!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){\\"undefined\\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\\"Module\\"}),Object.defineProperty(e,\\"__esModule\\",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&\\"object\\"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,\\"default\\",{enumerable:!0,value:e}),2&t&&\\"string\\"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,\\"a\\",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p=\\"\\",r(r.s=0)}([function(e,t,r){\\"use strict\\";r.r(t);console.log(\\"\\")}]);"`; 6 | 7 | exports[`Should run a webpack build with default options Webpack v5 1`] = `"f97721ce8640cb7a7086b565edd955a4c97de7c9"`; 8 | 9 | exports[`Should run a webpack build with default options Webpack v5 2`] = `"(()=>{\\"use strict\\";console.log(\\"\\")})();"`; 10 | -------------------------------------------------------------------------------- /src/__test__/basic/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs/promises'; 2 | import path from 'path'; 3 | import { DEFAULT_EXTENSIONS } from '../../extensions/default.extensions'; 4 | import { SquooshPlugin } from '../../index'; 5 | import { checksum } from '../util/checksum'; 6 | import { getDistDir } from '../util/getDistDir'; 7 | import { runWebpackBuild } from '../util/runWebpackBuild'; 8 | 9 | describe('Should run a webpack build with default options', () => { 10 | async function test(webpackVersion: 4 | 5) { 11 | const dist = getDistDir(__dirname, webpackVersion); 12 | 13 | await runWebpackBuild( 14 | webpackVersion, 15 | __dirname, 16 | { 17 | plugins: [ 18 | new SquooshPlugin({ 19 | preserveFileName: true, // Ensure output is consistent 20 | useWorker: false, // Not working with Jest 21 | outDir: dist, 22 | }), 23 | ], 24 | }, 25 | ); 26 | 27 | const expectedOutputFilePath = path.resolve(dist, 'logo.jpg'); 28 | const outputFileChecksum = await checksum(expectedOutputFilePath); 29 | const mainJs = await readFile(path.resolve(dist, 'main.js'), 'utf-8'); 30 | const stringifiedOutputFilePath = JSON.stringify(expectedOutputFilePath); 31 | 32 | expect(outputFileChecksum).toMatchSnapshot(); 33 | expect(mainJs).toContain(stringifiedOutputFilePath); 34 | expect(mainJs.replace(stringifiedOutputFilePath, '""')).toMatchSnapshot(); 35 | } 36 | 37 | const webpackVersions: [4, 5] = [4, 5]; 38 | 39 | it.each(webpackVersions)('Webpack v%p', test); 40 | }); 41 | -------------------------------------------------------------------------------- /src/__test__/basic/src/index.js: -------------------------------------------------------------------------------- 1 | import Logo from './logo.png'; 2 | 3 | console.log(Logo); 4 | -------------------------------------------------------------------------------- /src/__test__/basic/src/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcheidemann/squoosh-webpack-plugin/a7e9539188ed88e0b994518c5b842395e5596f64/src/__test__/basic/src/logo.png -------------------------------------------------------------------------------- /src/__test__/util/checksum.ts: -------------------------------------------------------------------------------- 1 | import { file } from 'checksum'; 2 | 3 | export async function checksum(filename: string) { 4 | return new Promise((resolve, reject) => { 5 | file(filename, (err, hash) => { 6 | if (err) reject(err); 7 | resolve(hash); 8 | }); 9 | }) 10 | } -------------------------------------------------------------------------------- /src/__test__/util/createWebpackConfig.ts: -------------------------------------------------------------------------------- 1 | import webpack4 from 'webpack4'; 2 | import webpack5 from 'webpack5'; 3 | import path from 'path'; 4 | import { getDistDir } from './getDistDir'; 5 | 6 | const createConfig = (webpackVersion: 4 | 5, dirname: string, config: T): T => ({ 7 | mode: 'production', 8 | ...config, 9 | entry: path.resolve(dirname, 'src', 'index.js'), 10 | output: { 11 | ...config.output, 12 | filename: 'main.js', 13 | path: getDistDir(dirname, webpackVersion), 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(png|jpe?g|gif)$/i, 19 | loader: require.resolve('./image-loader.js'), 20 | } 21 | ] 22 | } 23 | }) 24 | 25 | export const createWebpack4Config = ( 26 | dirname: string, 27 | config: webpack4.Configuration, 28 | ) => createConfig(4, dirname, config); 29 | 30 | export const createWebpack5Config = ( 31 | dirname: string, 32 | config: webpack5.Configuration, 33 | ) => createConfig(5, dirname, config); 34 | -------------------------------------------------------------------------------- /src/__test__/util/getDistDir.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | export function getDistDir(dirname: string, webpackVersion: 4 | 5) { 4 | return path.resolve(dirname, 'dist', `v${webpackVersion}`); 5 | } -------------------------------------------------------------------------------- /src/__test__/util/image-loader.js: -------------------------------------------------------------------------------- 1 | module.exports.pitch = function loader(request) { 2 | return `export default ${JSON.stringify(request)}`; 3 | } 4 | -------------------------------------------------------------------------------- /src/__test__/util/removeDistFolder.ts: -------------------------------------------------------------------------------- 1 | import { remove } from 'fs-extra'; 2 | import { getDistDir } from './getDistDir'; 3 | 4 | export async function removeDistFolder(dirname: string, webpackVersion: 4 | 5) { 5 | const dist = getDistDir(dirname, webpackVersion); 6 | return new Promise((resolve, reject) => { 7 | remove(dist, (err) => { 8 | if (err) reject(err); 9 | resolve(); 10 | }) 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/__test__/util/runWebpackBuild.ts: -------------------------------------------------------------------------------- 1 | import webpack4 from 'webpack4'; 2 | import webpack5 from 'webpack5'; 3 | 4 | import { createWebpack4Config, createWebpack5Config } from './createWebpackConfig'; 5 | import { removeDistFolder } from './removeDistFolder'; 6 | 7 | export async function runWebpackBuild(webpackVersion: 4 | 5, dirname: string, config: webpack4.Configuration | webpack5.Configuration = {}) { 8 | await removeDistFolder(dirname, webpackVersion); 9 | 10 | switch (webpackVersion) { 11 | case 4: 12 | return runWebpack4Build(dirname, config as webpack4.Configuration); 13 | case 5: 14 | return runWebpack5Build(dirname, config as webpack5.Configuration); 15 | default: 16 | throw new Error(`Webpack v${webpackVersion} is not supported`); 17 | } 18 | } 19 | 20 | async function runWebpack4Build(dirname: string, config: webpack4.Configuration = {}) { 21 | const _config = createWebpack4Config(dirname, config); 22 | return new Promise((resolve, reject) => { 23 | webpack4(_config, (err, stats) => { 24 | if (err) reject(err); 25 | resolve(stats); 26 | }) 27 | }); 28 | } 29 | 30 | async function runWebpack5Build(dirname: string, config: webpack5.Configuration = {}) { 31 | const _config = createWebpack5Config(dirname, config); 32 | return new Promise((resolve, reject) => { 33 | webpack5(_config, (err, stats) => { 34 | if (err) reject(err); 35 | resolve(stats); 36 | }) 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/extensions/BaseResolverExtension/BaseResolverExtension.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { Tokens } from '../../tokens'; 3 | import { RequestOptions, Extension, HookContext } from '../../types/extensions'; 4 | import { Match, match } from '../../utils/match'; 5 | 6 | export class BaseResolverExtension implements Extension { 7 | public name = 'BaseResolverExtension'; 8 | public order = Number.MIN_SAFE_INTEGER; // Ensure this extension runs first 9 | 10 | public request(context: HookContext, options: RequestOptions) { 11 | // Request is excluded by default 12 | options.include = false; 13 | 14 | let request = options.request; 15 | 16 | // If request prefix is defined then validate if the request based on this 17 | if (context.options.requestPrefix) { 18 | if (request.startsWith(context.options.requestPrefix)) { 19 | // Include the request 20 | options.include = true; 21 | 22 | // Trim the prefix 23 | request = request.substr(context.options.requestPrefix.length); 24 | } else { 25 | // Exclude the request 26 | options.include = false; 27 | 28 | // Hand over to next plugin 29 | return options; 30 | } 31 | } 32 | 33 | // Get the absolute path 34 | const absoluteRequestPath = resolve(options.context, request); 35 | 36 | // Validate if the request matches one of the dirs provided 37 | if (context.options.dirs) { 38 | const matched = context.options.dirs.some((dir) => { 39 | const detokenizedDir = dir.replace(Tokens.ROOT_DIR, process.cwd()); 40 | absoluteRequestPath.startsWith(resolve(process.cwd(), detokenizedDir)); 41 | }); 42 | 43 | if (!matched) { 44 | // Exclude the request 45 | options.include = false; 46 | 47 | // Hand over to next plugin 48 | return options; 49 | } 50 | } 51 | 52 | // Validate if any of the include regexs pass 53 | if (context.options.include) { 54 | const matchAny = match(context.options.include, absoluteRequestPath, { 55 | match: Match.ANY, 56 | }); 57 | 58 | if (matchAny) { 59 | // Exclude the request 60 | options.include = true; 61 | } 62 | } 63 | 64 | // Validate if any of the exclude regexs pass 65 | if (context.options.exclude) { 66 | const matchAny = match(context.options.exclude, absoluteRequestPath, { 67 | match: Match.ANY, 68 | }); 69 | 70 | if (matchAny) { 71 | // Exclude the request 72 | options.include = false; 73 | 74 | // Hand over to next plugin 75 | return options; 76 | } 77 | } 78 | 79 | return options; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/extensions/BaseResolverExtension/index.ts: -------------------------------------------------------------------------------- 1 | export { BaseResolverExtension as default } from './BaseResolverExtension'; -------------------------------------------------------------------------------- /src/extensions/BasicCacheExtension/BasicCacheExtension.ts: -------------------------------------------------------------------------------- 1 | import { PrepareOptions, Extension, HookContext } from "../../types/extensions"; 2 | import { ExtensionError } from '../extension-error'; 3 | import { exists } from '../../utils/exists'; 4 | 5 | export class BasicCacheExtension implements Extension { 6 | public name = 'BasicCacheExtension'; 7 | public order = Number.MAX_SAFE_INTEGER - 1; // Ensure this plugin after all other plugins except EnsureOutputDirectory; 8 | 9 | public async prepare(_: HookContext, options: PrepareOptions) { 10 | if (!options.outputPath) { 11 | throw new ExtensionError(this, 'Output path ("outputPath") must be defined by a preceding hook'); 12 | } 13 | 14 | options.skip = await exists(options.outputPath); 15 | 16 | return options; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/extensions/BasicCacheExtension/index.ts: -------------------------------------------------------------------------------- 1 | export { BasicCacheExtension as default } from './BasicCacheExtension'; 2 | -------------------------------------------------------------------------------- /src/extensions/DefaultOptionsExtension/DefaultOptionsExtension.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { 3 | InitializeOptions, 4 | Extension, 5 | HookContext, 6 | } from '../../types/extensions'; 7 | import { DEFAULT_OPTIONS } from './default-options'; 8 | 9 | export class DefaultOptionsExtension implements Extension { 10 | public name = 'DefaultOptionsExtension'; 11 | public order = Number.MIN_SAFE_INTEGER; // Ensure this extension runs first 12 | 13 | public initialize(_: HookContext, baseOptions: InitializeOptions) { 14 | const options = { 15 | ...DEFAULT_OPTIONS, 16 | ...baseOptions, 17 | }; 18 | 19 | return options; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/extensions/DefaultOptionsExtension/default-options.ts: -------------------------------------------------------------------------------- 1 | import { SquooshPluginOptions } from '../../types'; 2 | 3 | export const DEFAULT_OPTIONS: SquooshPluginOptions = { 4 | // Internal Options 5 | extensions: [], 6 | encoderOptions: {}, 7 | codec: 'mozjpeg', 8 | useWorker: true, 9 | 10 | // Used by BaseResolverExtension 11 | requestPrefix: undefined, 12 | include: /\.(jpeg|jpg|png)$/, 13 | exclude: undefined, 14 | dirs: undefined, 15 | 16 | // Used by DefaultOutputPathExtension 17 | outDir: undefined, 18 | uuidNamespace: 'bc707c24-ee40-4f77-816e-15f0c76a81de', 19 | preserveFileName: false, 20 | }; 21 | -------------------------------------------------------------------------------- /src/extensions/DefaultOptionsExtension/index.ts: -------------------------------------------------------------------------------- 1 | export { DefaultOptionsExtension as default } from './DefaultOptionsExtension'; -------------------------------------------------------------------------------- /src/extensions/DefaultOutputPathExtension/DefaultOutputPathExtension.ts: -------------------------------------------------------------------------------- 1 | import { basename, dirname, join, resolve } from 'path'; 2 | import { PrepareOptions, Extension, HookContext } from '../../types/extensions'; 3 | import { v5 as uuidV5 } from 'uuid'; 4 | import { codecExtensionMap } from '../../utils/extensions'; 5 | import { generateChecksum } from '../../utils/checksum'; 6 | import { replaceExtension } from '../../utils/replaceExtension'; 7 | import { Tokens } from '../../tokens'; 8 | 9 | export class DefaultOutputPathExtension implements Extension { 10 | public name = 'DefaultOutputPathExtension'; 11 | public order = Number.MAX_SAFE_INTEGER - 2; // Ensure this plugin after all other plugins except EnsureOutputDirectory and the BasicCacheExtension 12 | 13 | public async prepare(context: HookContext, options: PrepareOptions) { 14 | let outDir = context.options.outDir 15 | ? resolve(process.cwd(), context.options.outDir) 16 | : dirname(options.inputPath); 17 | outDir = outDir.replace(Tokens.ROOT_DIR, process.cwd()); 18 | const inputFileName = basename(options.inputPath); 19 | 20 | if (context.options.preserveFileName) { 21 | const outputFilename = replaceExtension(inputFileName, codecExtensionMap[options.codec]); 22 | 23 | options.outputPath = join( 24 | outDir, 25 | outputFilename, 26 | ); 27 | } 28 | else { 29 | const uuidNamespace = context.options.uuidNamespace; 30 | const serialisedEncoding = JSON.stringify(options.encoderOptions); 31 | const checksum = await generateChecksum(options.inputPath); 32 | const outputFileId = uuidV5( 33 | options.inputPath + serialisedEncoding + options.codec + checksum, 34 | uuidNamespace 35 | ); 36 | const outputFilename = `${inputFileName}.${outputFileId}.${codecExtensionMap[options.codec]}`; 37 | 38 | options.outputPath = join( 39 | outDir, 40 | outputFilename, 41 | ); 42 | } 43 | 44 | return options; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/extensions/DefaultOutputPathExtension/index.ts: -------------------------------------------------------------------------------- 1 | export { DefaultOutputPathExtension as default } from './DefaultOutputPathExtension'; 2 | -------------------------------------------------------------------------------- /src/extensions/EnsureOutputDirectoryExtension/EnsureOutputDirectoryExtension.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | import { ensureDir } from 'fs-extra'; 3 | import { PrepareOptions, Extension, HookContext } from "../../types/extensions"; 4 | import { ExtensionError } from '../extension-error'; 5 | 6 | export class EnsureOutputDirectoryExtension implements Extension { 7 | public name = 'EnsureOutputDirectoryExtension'; 8 | public order = Number.MAX_SAFE_INTEGER; // Ensure this plugin runs last 9 | 10 | public async prepare(_: HookContext, options: PrepareOptions) { 11 | if (!options.outputPath) { 12 | throw new ExtensionError(this, 'Output path ("outputPath") must be defined by a preceding hook'); 13 | } 14 | 15 | const outDir = dirname(options.outputPath); 16 | 17 | await ensureDir(outDir); 18 | 19 | return options; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/extensions/EnsureOutputDirectoryExtension/index.ts: -------------------------------------------------------------------------------- 1 | export { EnsureOutputDirectoryExtension as default } from './EnsureOutputDirectoryExtension'; -------------------------------------------------------------------------------- /src/extensions/default.extensions.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../types/extensions'; 2 | import DefaultOptionsExtension from './DefaultOptionsExtension'; 3 | import BaseResolverExtension from './BaseResolverExtension'; 4 | import DefaultOutputPathExtension from './DefaultOutputPathExtension'; 5 | import BasicCacheExtension from './BasicCacheExtension'; 6 | import EnsureOutputDirectoryExtension from './EnsureOutputDirectoryExtension'; 7 | 8 | // These define the default behaviour of the SquooshWebpackPlugin 9 | export const DEFAULT_EXTENSIONS: Array> = [ 10 | new DefaultOptionsExtension(), 11 | new BaseResolverExtension(), 12 | new DefaultOutputPathExtension(), 13 | new BasicCacheExtension(), 14 | new EnsureOutputDirectoryExtension(), 15 | ]; 16 | -------------------------------------------------------------------------------- /src/extensions/extension-error.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from "../types/extensions"; 2 | 3 | export class ExtensionError extends Error { 4 | constructor(extension: Extension, message: string) { 5 | super(`Error in ${extension.name || '[Anonymous Extension]'}: ${message}`); 6 | this.name = 'ExtensionError'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/extensions/index.ts: -------------------------------------------------------------------------------- 1 | export * as BaseResolverExtension from './BaseResolverExtension'; 2 | export * as BasicCacheExtension from './BasicCacheExtension'; 3 | export * as DefaultOptionsExtension from './DefaultOptionsExtension'; 4 | export * as DefaultOutputPathExtension from './DefaultOutputPathExtension'; 5 | export * as EnsureOutputDirectory from './EnsureOutputDirectoryExtension'; 6 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare module '@squoosh/lib' { 5 | export class ImagePool { 6 | async close(); 7 | ingestImage(path: string); 8 | } 9 | 10 | export const encoders: Record< 11 | string, 12 | { 13 | defaultEncoderOptions: any; 14 | } 15 | >; 16 | } 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { ChildProcess, fork } from 'child_process'; 3 | import * as uuid from 'uuid'; 4 | import { Codecs, SquooshPluginOptions } from './types'; 5 | import { WorkerEvents } from './worker/events'; 6 | import { 7 | WorkerRequest, 8 | WorkerRequestData, 9 | WorkerResponse, 10 | WorkerResponseData, 11 | } from './worker/types'; 12 | import { Compiler as Compiler4, Plugin } from 'webpack4'; 13 | import { Compiler as Compiler5, WebpackPluginInstance } from 'webpack5'; 14 | import { DEFAULT_EXTENSIONS } from './extensions/default.extensions'; 15 | import { sortLowHigh } from './utils/sort'; 16 | import { Extension, HookContext, PrepareOptions, RequestOptions } from './types/extensions'; 17 | import { handlers } from './worker'; 18 | import { getWebpackVersion } from './utils/getWebpackVersion'; 19 | import { ResolveData } from './types/webpack'; 20 | 21 | export * from './types'; 22 | 23 | const workerPath = require.resolve('./worker'); 24 | 25 | const PLUGIN_NAME = 'squoosh-webpack-plugin'; 26 | 27 | export class SquooshPlugin { 28 | private workerProcess: ChildProcess | null = null; 29 | private options: Promise; 30 | 31 | constructor(options?: Partial>) { 32 | this.options = this.validateOptions(options as SquooshPluginOptions); 33 | } 34 | 35 | private async emitToWorker( 36 | event: Event, 37 | data: WorkerRequestData 38 | ): Promise> { 39 | const options = await this.options; 40 | if (options.useWorker) { 41 | if (!this.workerProcess) { 42 | this.workerProcess = fork(workerPath); 43 | } 44 | const worker = this.workerProcess as ChildProcess; 45 | return await new Promise>((resolve, reject) => { 46 | const request: WorkerRequest = { 47 | event, 48 | data, 49 | id: uuid.v4(), 50 | }; 51 | const handler = (response: WorkerResponse) => { 52 | if (typeof response !== 'object') return; 53 | if (response.id === request.id) { 54 | worker.off('message', handler); 55 | if (response.event === request.event) { 56 | resolve(response.data); 57 | } else if (response.event === WorkerEvents.error) { 58 | reject( 59 | new Error( 60 | [ 61 | `Error in image-optimise.worker.js.`, 62 | ` Error occurred in event the handler for event: ${request.event}.`, 63 | ` Handler responded with the error:`, 64 | ` ${response.data}`, 65 | ].join('\n') 66 | ) 67 | ); 68 | } 69 | } 70 | }; 71 | worker.send(request); 72 | worker.on('message', handler); 73 | }); 74 | } 75 | else { 76 | return await Promise.resolve(handlers[event](data)); 77 | } 78 | } 79 | 80 | apply(compiler: any) { 81 | switch (getWebpackVersion(compiler)) { 82 | case 4: 83 | return this.webpack4Apply(compiler); 84 | case 5: 85 | return this.webpack5Apply(compiler); 86 | } 87 | } 88 | 89 | private webpack4Apply(compiler: Compiler4) { 90 | compiler.hooks.beforeCompile.tapPromise(PLUGIN_NAME, this.handleBeforeCompile); 91 | 92 | compiler.hooks.normalModuleFactory.tap(PLUGIN_NAME, (factory) => { 93 | factory.hooks.beforeResolve.tapPromise(PLUGIN_NAME, this.handleResolve); 94 | }); 95 | 96 | compiler.hooks.done.tapPromise(PLUGIN_NAME, this.handleStop); 97 | } 98 | 99 | private webpack5Apply(compiler: Compiler5) { 100 | compiler.hooks.beforeCompile.tapPromise(PLUGIN_NAME, this.handleBeforeCompile); 101 | 102 | compiler.hooks.normalModuleFactory.tap(PLUGIN_NAME, (factory) => { 103 | factory.hooks.resolve.tapPromise(PLUGIN_NAME, this.handleResolve); 104 | }); 105 | 106 | compiler.hooks.done.tapPromise(PLUGIN_NAME, this.handleStop); 107 | } 108 | 109 | private handleBeforeCompile = async () => { 110 | await this.emitToWorker(WorkerEvents.start, null); 111 | }; 112 | 113 | private handleResolve = async (resolveData: ResolveData) => { 114 | const baseRequest: RequestOptions = { 115 | include: false, // Exclude by default 116 | context: resolveData.context, 117 | request: resolveData.request, 118 | }; 119 | const requestData = await this.applyRequestHooks(baseRequest); 120 | 121 | if (requestData.include) { 122 | const options = await this.options; 123 | const inputPath = resolve(requestData.context, requestData.request); 124 | const processOptions: PrepareOptions = await this.applyPrepareHooks({ 125 | skip: false, // No caching by default 126 | inputPath, 127 | outputPath: undefined, 128 | codec: options.codec, 129 | encoderOptions: options.encoderOptions, 130 | }); 131 | 132 | if (!processOptions.outputPath) { 133 | throw new Error('At least one "prepare" hook must set the "outputPath".'); 134 | } 135 | 136 | resolveData.request = processOptions.outputPath; 137 | 138 | if (!processOptions.skip) { 139 | const processRequest: WorkerRequestData = { 140 | inputPath: processOptions.inputPath, 141 | outputPath: processOptions.outputPath, 142 | codec: processOptions.codec, 143 | encoderOptions: processOptions.encoderOptions, 144 | }; 145 | await this.emitToWorker( 146 | WorkerEvents.process, 147 | processRequest, 148 | ); 149 | } 150 | } 151 | }; 152 | 153 | private handleStop = async () => { 154 | await this.emitToWorker(WorkerEvents.stop, null); 155 | if (this.workerProcess) { 156 | this.workerProcess.kill(); 157 | } 158 | }; 159 | 160 | private async validateOptions(options?: Partial) { 161 | const baseOptions = { 162 | ...options, 163 | }; 164 | 165 | if (baseOptions.extensions) { 166 | switch (typeof baseOptions.extensions) { 167 | case 'function': 168 | baseOptions.extensions = baseOptions.extensions(DEFAULT_EXTENSIONS); 169 | if (!Array.isArray(baseOptions.extensions)) { 170 | throw new Error( 171 | 'Config Error: "extensions" must return an Array of Extensions.' 172 | ); 173 | } 174 | break; 175 | case 'object': 176 | if (Array.isArray(baseOptions.extensions)) { 177 | } 178 | default: 179 | throw new Error( 180 | 'Config Error: Type of "extensions" must be either Array or Function.' 181 | ); 182 | } 183 | } else { 184 | baseOptions.extensions = DEFAULT_EXTENSIONS; 185 | } 186 | 187 | baseOptions.extensions = sortLowHigh( 188 | baseOptions.extensions, 189 | (extension) => extension.order || 0 190 | ); 191 | 192 | return await this.applyInitializeHooks(baseOptions); 193 | } 194 | 195 | private async applyInitializeHooks( 196 | baseOptions: Partial 197 | ) { 198 | const extensions = baseOptions.extensions as Array; 199 | 200 | let options = baseOptions; 201 | for (const extension of extensions) { 202 | if (extension.initialize) { 203 | const context = Object.freeze({ 204 | options, 205 | }) as HookContext; 206 | options = await Promise.resolve(extension.initialize(context, options)); 207 | } 208 | } 209 | return options as SquooshPluginOptions; 210 | } 211 | 212 | private async applyRequestHooks(baseRequest: RequestOptions) { 213 | const options = await this.options; 214 | const context: HookContext = Object.freeze({ 215 | options, 216 | }); 217 | const extensions = options.extensions as Array; 218 | 219 | let request = baseRequest; 220 | for (const extension of extensions) { 221 | if (extension.request) { 222 | request = await Promise.resolve(extension.request(context, request)); 223 | } 224 | } 225 | 226 | return request; 227 | } 228 | 229 | private async applyPrepareHooks(baseOptions: PrepareOptions) { 230 | const options = await this.options; 231 | const context: HookContext = Object.freeze({ 232 | options, 233 | }); 234 | const extensions = options.extensions as Array; 235 | 236 | let prepareOptions = baseOptions; 237 | for (const extension of extensions) { 238 | if (extension.prepare) { 239 | prepareOptions = await Promise.resolve(extension.prepare(context, prepareOptions)); 240 | } 241 | } 242 | 243 | return prepareOptions; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/tokens/index.ts: -------------------------------------------------------------------------------- 1 | export enum Tokens { 2 | ROOT_DIR = '', 3 | } 4 | -------------------------------------------------------------------------------- /src/types/encoder-options.ts: -------------------------------------------------------------------------------- 1 | import { AvifEncodeOptions } from '.'; 2 | import { 3 | JxlEncodeOptions, 4 | MozJPEGEncodeOptions, 5 | OxiPngEncodeOptions, 6 | WebPEncodeOptions, 7 | WP2EncodeOptions, 8 | } from './squoosh'; 9 | 10 | export type Codecs = 'mozjpeg' | 'webp' | 'avif' | 'jxl' | 'wp2' | 'oxipng'; 11 | 12 | export type SquooshEncodeOptions = 13 | Codec extends 'mozjpeg' 14 | ? Partial 15 | : Codec extends 'webp' 16 | ? Partial 17 | : Codec extends 'avif' 18 | ? Partial 19 | : Codec extends 'jxl' 20 | ? Partial 21 | : Codec extends 'wp2' 22 | ? Partial 23 | : Codec extends 'oxipng' 24 | ? Partial 25 | : never; 26 | -------------------------------------------------------------------------------- /src/types/extensions/extension.ts: -------------------------------------------------------------------------------- 1 | import { PrepareHook, RequestHook } from "./hooks"; 2 | import { Codecs } from "../encoder-options"; 3 | import { InitializeHook } from "./hooks"; 4 | 5 | export interface Extension { 6 | name?: string; 7 | order?: number; 8 | initialize?: InitializeHook; 9 | request?: RequestHook; 10 | prepare?: PrepareHook; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/extensions/hooks.ts: -------------------------------------------------------------------------------- 1 | import { SquooshPluginOptions } from "../plugin-options"; 2 | import { Codecs, SquooshEncodeOptions } from "../encoder-options"; 3 | 4 | export interface InitializeOptions extends Partial> {} 5 | 6 | export interface RequestOptions { 7 | include: boolean; 8 | context: string; 9 | request: string; 10 | } 11 | 12 | export interface PrepareOptions { 13 | skip: boolean; // this can be used for caching 14 | inputPath: string; 15 | outputPath?: string; 16 | codec: Codec; 17 | encoderOptions: SquooshEncodeOptions; 18 | } 19 | 20 | export interface HookContext { 21 | options: SquooshPluginOptions; 22 | } 23 | 24 | export type Hook = (context: C, request: T) => T | Promise; 25 | 26 | /** 27 | * Initialize Hook 28 | * 29 | * This is invoked once upon instantiating the `SquooshPlugin` class. 30 | * It will receive the an options object of type `SquooshPluginOptions`. 31 | */ 32 | export type InitializeHook = Hook, InitializeOptions>; 33 | 34 | /** 35 | * Request Hook 36 | * 37 | * This is invoked for every request that is resolved by 38 | * Webpack. It will receive: 39 | * - context (the directory the resource is being requested from) 40 | * - request (usually the relative file path or a node module) 41 | * - include (set this to true if the resource should be processed by Squoosh) 42 | */ 43 | export type RequestHook = Hook, RequestOptions>; 44 | 45 | /** 46 | * Prepare Hook 47 | * 48 | * This is invoked after the Request Hook. It will receive: 49 | */ 50 | export type PrepareHook = Hook, PrepareOptions>; 51 | -------------------------------------------------------------------------------- /src/types/extensions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | export * from './extension'; 3 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export { SquooshPluginOptions } from './plugin-options'; 2 | export { SquooshEncodeOptions, Codecs } from './encoder-options'; 3 | export { 4 | MozJpegColorSpace, 5 | MozJPEGEncodeOptions, 6 | WebPEncodeOptions, 7 | AVIFTune, 8 | AvifEncodeOptions, 9 | JxlEncodeOptions, 10 | UVMode, 11 | Csp, 12 | WP2EncodeOptions, 13 | OxiPngEncodeOptions, 14 | } from './squoosh'; 15 | export * from './extensions'; 16 | -------------------------------------------------------------------------------- /src/types/plugin-options.ts: -------------------------------------------------------------------------------- 1 | import { SquooshEncodeOptions, Codecs } from './encoder-options'; 2 | import { Extension } from './extensions'; 3 | 4 | export type SquooshPluginOptions = { 5 | // Internal Options 6 | extensions: 7 | | Array> 8 | | ((defaultExtensions: Array) => Array); 9 | encoderOptions: SquooshEncodeOptions; 10 | codec: Codec; 11 | useWorker: boolean; 12 | 13 | // Used by BaseResolverExtension 14 | requestPrefix?: string; 15 | include?: RegExp | Array; 16 | exclude?: RegExp | Array; 17 | dirs?: Array; 18 | 19 | // Used by DefaultOutputPathExtension 20 | outDir?: string; 21 | uuidNamespace: string; 22 | preserveFileName?: boolean; 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/squoosh.ts: -------------------------------------------------------------------------------- 1 | // SOURCE: https://github.com/GoogleChromeLabs/squoosh/tree/dev/libsquoosh 2 | 3 | export const enum MozJpegColorSpace { 4 | GRAYSCALE = 1, 5 | RGB, 6 | YCbCr, 7 | } 8 | 9 | export interface MozJPEGEncodeOptions { 10 | quality: number; 11 | baseline: boolean; 12 | arithmetic: boolean; 13 | progressive: boolean; 14 | optimize_coding: boolean; 15 | smoothing: number; 16 | color_space: MozJpegColorSpace; 17 | quant_table: number; 18 | trellis_multipass: boolean; 19 | trellis_opt_zero: boolean; 20 | trellis_opt_table: boolean; 21 | trellis_loops: number; 22 | auto_subsample: boolean; 23 | chroma_subsample: number; 24 | separate_chroma_quality: boolean; 25 | chroma_quality: number; 26 | } 27 | 28 | export interface WebPEncodeOptions { 29 | quality: number; 30 | target_size: number; 31 | target_PSNR: number; 32 | method: number; 33 | sns_strength: number; 34 | filter_strength: number; 35 | filter_sharpness: number; 36 | filter_type: number; 37 | partitions: number; 38 | segments: number; 39 | pass: number; 40 | show_compressed: number; 41 | preprocessing: number; 42 | autofilter: number; 43 | partition_limit: number; 44 | alpha_compression: number; 45 | alpha_filtering: number; 46 | alpha_quality: number; 47 | lossless: number; 48 | exact: number; 49 | image_hint: number; 50 | emulate_jpeg_size: number; 51 | thread_level: number; 52 | low_memory: number; 53 | near_lossless: number; 54 | use_delta_palette: number; 55 | use_sharp_yuv: number; 56 | } 57 | 58 | export const enum AVIFTune { 59 | auto, 60 | psnr, 61 | ssim, 62 | } 63 | 64 | export interface AvifEncodeOptions { 65 | cqLevel: number; 66 | denoiseLevel: number; 67 | cqAlphaLevel: number; 68 | tileRowsLog2: number; 69 | tileColsLog2: number; 70 | speed: number; 71 | subsample: number; 72 | chromaDeltaQ: boolean; 73 | sharpness: number; 74 | tune: AVIFTune; 75 | } 76 | 77 | export interface JxlEncodeOptions { 78 | effort: number; 79 | quality: number; 80 | progressive: boolean; 81 | epf: number; 82 | lossyPalette: boolean; 83 | decodingSpeedTier: number; 84 | photonNoiseIso: number; 85 | } 86 | 87 | export const enum UVMode { 88 | UVModeAdapt = 0, // Mix of 420 and 444 (per block) 89 | UVMode420, // All blocks 420 90 | UVMode444, // All blocks 444 91 | UVModeAuto, // Choose any of the above automatically 92 | } 93 | 94 | export const enum Csp { 95 | kYCoCg, 96 | kYCbCr, 97 | kCustom, 98 | kYIQ, 99 | } 100 | 101 | export interface WP2EncodeOptions { 102 | quality: number; 103 | alpha_quality: number; 104 | effort: number; 105 | pass: number; 106 | sns: number; 107 | uv_mode: UVMode; 108 | csp_type: Csp; 109 | error_diffusion: number; 110 | use_random_matrix: boolean; 111 | } 112 | 113 | export interface OxiPngEncodeOptions { 114 | level: number; 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/types/webpack.ts: -------------------------------------------------------------------------------- 1 | export interface ResolveData { 2 | [key: string]: any; 3 | context: string; 4 | request: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/checksum.ts: -------------------------------------------------------------------------------- 1 | import { file as checksum } from 'checksum'; 2 | 3 | export async function generateChecksum(path: string) { 4 | return await new Promise((resolve, reject) => { 5 | checksum(path, (error, hash) => { 6 | if (error) { 7 | reject(error); 8 | } 9 | resolve(hash); 10 | }); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/exists.ts: -------------------------------------------------------------------------------- 1 | import { stat } from "fs-extra"; 2 | 3 | export async function exists(path: string) { 4 | // Check if the output file path exists 5 | try { 6 | await stat(path); 7 | // If the file exists then return true 8 | return true; 9 | } catch (err: any) { 10 | // Throw any other errors than file not found 11 | if (err.code !== 'ENOENT') { 12 | throw err; 13 | } 14 | // If the file doesn't exist then return false 15 | return false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/extensions.ts: -------------------------------------------------------------------------------- 1 | import { Codecs } from "../types"; 2 | 3 | export const codecExtensionMap: Record = { 4 | mozjpeg: 'jpg', 5 | webp: 'webp', 6 | avif: 'avif', 7 | jxl: 'jxl', 8 | wp2: 'wp2', 9 | oxipng: 'png', 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/getWebpackVersion.ts: -------------------------------------------------------------------------------- 1 | import { Compiler as Compiler4 } from 'webpack4'; 2 | import { Compiler as Compiler5 } from 'webpack5'; 3 | 4 | export function getWebpackVersion(compiler: Compiler4 | Compiler5) { 5 | if (!compiler.hooks) { 6 | throw new Error(`[SquooshWebpackPlugin] Webpack version is not supported!`); 7 | } 8 | 9 | // Webpack v5+ implements compiler caching 10 | return 'cache' in compiler ? 5 : 4; 11 | }; -------------------------------------------------------------------------------- /src/utils/match.ts: -------------------------------------------------------------------------------- 1 | export enum Match { 2 | NONE, 3 | ANY, 4 | ALL, 5 | } 6 | 7 | type MatchOptions = { 8 | match: Match; 9 | }; 10 | 11 | export function match( 12 | regex: RegExp | Array, 13 | string: string, 14 | options: MatchOptions = { match: Match.ANY } 15 | ) { 16 | if (Array.isArray(regex)) { 17 | switch (options.match) { 18 | case Match.NONE: 19 | return regex.every((regex) => !regex.test(string)); 20 | case Match.ANY: 21 | return regex.some((regex) => regex.test(string)); 22 | case Match.ALL: 23 | return regex.every((regex) => regex.test(string)); 24 | default: 25 | throw new Error('Invalid option: options.match'); 26 | } 27 | } else { 28 | const isMatch = regex.test(string); 29 | switch (options.match) { 30 | case Match.NONE: 31 | return !isMatch; 32 | case Match.ANY: 33 | case Match.ALL: 34 | return isMatch; 35 | default: 36 | throw new Error('Invalid option: options.match'); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/replaceExtension.ts: -------------------------------------------------------------------------------- 1 | export function replaceExtension(filename: string, extension: string) { 2 | return `${filename.substr(0, filename.lastIndexOf("."))}.${extension}`; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/sort.ts: -------------------------------------------------------------------------------- 1 | export function sortLowHigh(array: Array, evalutate: (item: T) => number): Array { 2 | return array.sort((a, b) => evalutate(a) - evalutate(b)); 3 | } 4 | 5 | export function sortHighLow(array: Array, evalutate: (item: T) => number): Array { 6 | return array.sort((a, b) => evalutate(a) - evalutate(b)); 7 | } 8 | -------------------------------------------------------------------------------- /src/worker/events.ts: -------------------------------------------------------------------------------- 1 | export enum WorkerEvents { 2 | error, 3 | start, 4 | stop, 5 | process, 6 | } 7 | -------------------------------------------------------------------------------- /src/worker/index.ts: -------------------------------------------------------------------------------- 1 | import { ImagePool, encoders } from '@squoosh/lib'; 2 | import { writeFile } from 'fs-extra'; 3 | import { WorkerEvents } from './events'; 4 | import { WorkerRequest, WorkerRequestData, WorkerResponseData } from './types'; 5 | 6 | type MaybePromise = Promise | T; 7 | type WorkerEventHandler = ( 8 | data: WorkerRequestData 9 | ) => MaybePromise>; 10 | type WorkerEventHandlers = { 11 | [Event in WorkerEvents]: WorkerEventHandler; 12 | }; 13 | 14 | let imagePool: ImagePool | undefined; 15 | 16 | export const handlers: WorkerEventHandlers = { 17 | [WorkerEvents.error]() {}, 18 | [WorkerEvents.start]() { 19 | if (!imagePool) { 20 | imagePool = new ImagePool(); 21 | } 22 | return null; 23 | }, 24 | async [WorkerEvents.stop]() { 25 | if (imagePool) { 26 | await imagePool.close(); 27 | imagePool = undefined; 28 | } 29 | return null; 30 | }, 31 | async [WorkerEvents.process]({ 32 | inputPath, 33 | outputPath, 34 | codec, 35 | encoderOptions, 36 | }) { 37 | // Ensure image pool was initialised 38 | if (!imagePool) { 39 | throw new Error('Image pool was not initialised.'); 40 | } 41 | 42 | // Create squoosh encoder options object 43 | const squooshEncoderOptions = { 44 | [codec]: { 45 | ...encoders[codec].defaultEncoderOptions, 46 | ...encoderOptions, 47 | }, 48 | }; 49 | 50 | // Encode the image 51 | const image = imagePool.ingestImage(inputPath); 52 | await image.decoded; 53 | await image.preprocess(); 54 | await image.encode(squooshEncoderOptions); 55 | const { binary: rawEncodedImage } = await image.encodedWith[codec]; 56 | 57 | // Save the file 58 | await writeFile(outputPath, rawEncodedImage); 59 | return null; 60 | }, 61 | }; 62 | 63 | process.on( 64 | 'message', 65 | (request: WorkerRequest) => { 66 | if (typeof request !== 'object') return; 67 | const { event, data, id } = request; 68 | const handler = handlers[event]; 69 | if (handler) { 70 | const responseOrPromise = handler(data); 71 | Promise.resolve(responseOrPromise) 72 | .then((response) => { 73 | process.send?.({ 74 | event, 75 | data: response, 76 | id, 77 | }); 78 | }) 79 | .catch((err) => { 80 | process.send?.({ 81 | event: 'error', 82 | data: err.message, 83 | id, 84 | }); 85 | console.error(err); 86 | }); 87 | } else { 88 | process.send?.({ 89 | event: 'error', 90 | data: `Unhandled event in squoosh worker (${event}).`, 91 | id, 92 | }); 93 | } 94 | } 95 | ); 96 | -------------------------------------------------------------------------------- /src/worker/types.ts: -------------------------------------------------------------------------------- 1 | import { Codecs, SquooshEncodeOptions } from '../types'; 2 | import { WorkerEvents } from './events'; 3 | 4 | export type WorkerRequestDataTypes = { 5 | [WorkerEvents.error]: any; 6 | [WorkerEvents.start]: null; 7 | [WorkerEvents.stop]: null; 8 | [WorkerEvents.process]: { 9 | inputPath: string; 10 | outputPath: string; 11 | codec: Codecs; 12 | encoderOptions: SquooshEncodeOptions; 13 | }; 14 | }; 15 | 16 | export type WorkerRequestData = 17 | WorkerRequestDataTypes[Event]; 18 | 19 | export type WorkerRequest = { 20 | event: Event; 21 | data: WorkerRequestData; 22 | id: string; 23 | }; 24 | 25 | export type WorkerResponseDataTypes = { 26 | [WorkerEvents.error]: any; 27 | [WorkerEvents.start]: null; 28 | [WorkerEvents.stop]: null; 29 | [WorkerEvents.process]: null; 30 | }; 31 | 32 | export type WorkerResponseData = 33 | WorkerResponseDataTypes[Event]; 34 | 35 | export type WorkerResponse = { 36 | event: Event; 37 | data: WorkerResponseData; 38 | id: string; 39 | }; 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "exclude": ["scripts/**/*"], 4 | "compilerOptions": { 5 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 6 | 7 | /* Projects */ 8 | // "incremental": true, /* Enable incremental compilation */ 9 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 10 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 11 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 12 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 13 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 14 | 15 | /* Language and Environment */ 16 | "target": "es5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 17 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 18 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 19 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 20 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 21 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 22 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 23 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 24 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 25 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 26 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 27 | 28 | /* Modules */ 29 | "module": "commonjs" /* Specify what module code is generated. */, 30 | "rootDir": "./src" /* Specify the root folder within your source files. */, 31 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 32 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 33 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 34 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 35 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 36 | "types": [ 37 | "node" 38 | ] /* Specify type package names to be included without being referenced in a source file. */, 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // "resolveJsonModule": true, /* Enable importing .json files */ 41 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 42 | 43 | /* JavaScript Support */ 44 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 45 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 46 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 47 | 48 | /* Emit */ 49 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 50 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 51 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 52 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 53 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 54 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 55 | // "removeComments": true, /* Disable emitting comments. */ 56 | // "noEmit": true, /* Disable emitting files from a compilation. */ 57 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 58 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 59 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 60 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 61 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 62 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 63 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 64 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 65 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 66 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 67 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 68 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 69 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 70 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 71 | 72 | /* Interop Constraints */ 73 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 74 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 75 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 76 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 77 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 78 | 79 | /* Type Checking */ 80 | "strict": true /* Enable all strict type-checking options. */, 81 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 82 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 83 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 84 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 85 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 86 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 87 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 88 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 89 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 90 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 91 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 92 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 93 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 94 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 95 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 96 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 97 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 98 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 99 | 100 | /* Completeness */ 101 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 102 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tsconfig.scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig.json", 3 | "include": ["scripts/**/*"], 4 | "exclude": ["src/**/*"], 5 | "compilerOptions": { 6 | "outDir": "scripts/dist", 7 | "declaration": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/index.ts"], 3 | "out": "docs" 4 | } 5 | --------------------------------------------------------------------------------