├── .eslintrc.js
├── .github
└── workflows
│ ├── e2e.js.yml
│ ├── e2ebasepath.js.yml
│ └── test.js.yml
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc
├── .swcrc
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── environment.d.ts
├── example
├── .eslintrc.json
├── .gitignore
├── README.md
├── app
│ ├── appdir
│ │ ├── page.module.css
│ │ └── page.tsx
│ ├── globals.css
│ └── layout.tsx
├── assets
│ └── chris-zhang-Jq8-3Bmh1pQ-unsplash_static_asset.jpg
├── environment.d.ts
├── next-env.d.ts
├── next.config.ts
├── package-lock.json
├── package.json
├── pages
│ ├── _app.js
│ ├── fixedImage.js
│ ├── forwardRef.js
│ ├── gifs.js
│ ├── index.js
│ ├── nested
│ │ ├── page.js
│ │ └── page_fixed.js
│ ├── nestedSlug
│ │ └── [slug].js
│ ├── remote.js
│ ├── smallImage.js
│ ├── subfolder.js
│ ├── transparent.js
│ └── typescript.tsx
├── public
│ ├── animated.png
│ ├── chris-zhang-Jq8-3Bmh1pQ-unsplash_static.jpg
│ ├── favicon.ico
│ ├── images
│ │ ├── 402107790_STATIC_NOISE_GIF.gif
│ │ ├── 402107790_STATIC_NOISE_WEBP.webp
│ │ ├── chris-zhang-Jq8-3Bmh1pQ-unsplash.jpg
│ │ ├── chris-zhang-Jq8-3Bmh1pQ-unsplash_small.jpg
│ │ ├── next-image-export-optimizer-hashes.json
│ │ ├── subfolder
│ │ │ ├── ollie-barker-jones-K52HVSPVvKI-unsplash.jpg
│ │ │ └── subfolder2
│ │ │ │ └── ollie-barker-jones-K52HVSPVvKI-unsplash.jpg
│ │ └── transparentImage.png
│ └── vercel.svg
├── remoteOptimizedImages.js
├── src
│ ├── ExportedImage.tsx
│ └── legacy
│ │ └── ExportedImage.tsx
├── styles
│ ├── Home.module.css
│ └── globals.css
├── test
│ └── e2e
│ │ ├── fixedImageSizeTest.spec.mjs
│ │ ├── getImageById.js
│ │ ├── imageSizeTest.spec.mjs
│ │ └── unoptimizedTest.spec.mjs
├── testServer.js
└── tsconfig.json
├── package-lock.json
├── package.json
├── playwright-basePath.config.js
├── playwright.config.js
├── src
├── optimizeImages.ts
└── utils
│ ├── ImageObject.ts
│ ├── defineProgressBar.ts
│ ├── downloadImagesInBatches.ts
│ ├── ensureDirectoryExists.ts
│ ├── getAllFilesAsObject.ts
│ ├── getHash.ts
│ ├── getRemoteImageURLs.ts
│ └── urlToFilename.ts
├── test
├── __snapshots__
│ └── optimizeImages.test.js.snap
└── optimizeImages.test.js
├── tsconfig.json
└── tsconfig.optimizeImages.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true,
6 | },
7 | extends: ["eslint:recommended", "plugin:react/recommended"],
8 | parserOptions: {
9 | ecmaFeatures: {
10 | jsx: true,
11 | },
12 | ecmaVersion: 13,
13 | sourceType: "module",
14 | },
15 | //ignore the dist folder
16 | ignorePatterns: ["dist/"],
17 | plugins: ["react"],
18 | rules: { "react/react-in-jsx-scope": "off" },
19 | };
20 |
--------------------------------------------------------------------------------
/.github/workflows/e2e.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Test e2e
5 |
6 | on:
7 | push:
8 | branches: ["master"]
9 | pull_request:
10 | branches: ["master"]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version: [20.x]
19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: "npm"
28 | - run: npm ci
29 | - run: cd example/ && npm ci && cd ..
30 | - run: npm run build && npm run test:e2e
31 |
--------------------------------------------------------------------------------
/.github/workflows/e2ebasepath.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Test e2e basePath
5 |
6 | on:
7 | push:
8 | branches: ["master"]
9 | pull_request:
10 | branches: ["master"]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version: [20.x]
19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: "npm"
28 | - run: npm ci
29 | - run: cd example/ && npm ci && cd ..
30 | - run: npm run build && npm run test:e2e:basePath
31 |
--------------------------------------------------------------------------------
/.github/workflows/test.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Test image generation
5 |
6 | on:
7 | push:
8 | branches: ["master"]
9 | pull_request:
10 | branches: ["master"]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version: [20.x]
19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: "npm"
28 | - run: npm ci
29 | - run: cd example/ && npm ci && cd ..
30 | - run: npm run build && npm test
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | example/public/images/subfolder/nextImageExportOptimizer
5 | example/public/images/subfolder/subfolder2/nextImageExportOptimizer
6 | example/public/images/nextImageExportOptimizer
7 | example/public/nextImageExportOptimizer
8 | example/remoteImagesForOptimization
9 | example/.vscode/settings.json
10 | test-results/.last-run.json
11 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | example
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | example/.next
2 | example/out
3 | example/public
4 | *.css
5 | .vscode
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": false
6 | }
7 |
--------------------------------------------------------------------------------
/.swcrc:
--------------------------------------------------------------------------------
1 | {
2 | "jsc": {
3 | "parser": {
4 | "syntax": "typescript",
5 | "tsx": true,
6 | "decorators": false,
7 | "dynamicImport": false
8 | },
9 | "target": "es5",
10 | "loose": false,
11 | "minify": {
12 | "compress": false,
13 | "mangle": false
14 | }
15 | },
16 | "module": {
17 | "type": "commonjs"
18 | },
19 | "minify": false
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "gifs",
4 | "networkidle",
5 | "unoptimized"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Niels Grafen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next-Image-Export-Optimizer
2 |
3 | [](https://www.npmjs.com/package/next-image-export-optimizer)
4 |
5 | Use [Next.js advanced **\** component](https://nextjs.org/docs/basic-features/image-optimization) with the static export functionality. Optimizes all static images in an additional step after the Next.js static export.
6 |
7 | - Reduces the image size and page load times drastically through responsive images
8 | - Fast image transformation using [sharp.js](https://www.npmjs.com/package/sharp) (also used by the Next.js server in production)
9 | - Conversion of JPEG and PNG files to the modern WEBP format by default
10 | - Serve the exported React bundle only via a CDN. No server required
11 | - Automatic generation of tiny, blurry placeholder images
12 | - Minimal configuration necessary
13 | - Supports TypeScript
14 | - Supports remote images which will be downloaded and optimized
15 | - Supports animated images (accepted formats: GIF and WEBP)
16 | - Note that only one global value can be used for the image quality setting. The default value is 75.
17 |
18 | ## Placement of the images:
19 |
20 | **For images using a path string:** (e.g. src="/profile.png")
21 |
22 | Place the images in a folder inside the public folder like _public/images_
23 |
24 | **For images using a static import:** (e.g. src={profileImage})
25 |
26 | You can place the images anywhere in your project. The images will be optimized and copied to the export folder.
27 |
28 | **For remote images:** (e.g. src="https://example.com/image.jpg")
29 |
30 | Please refer to the section on remote images.
31 |
32 | ## Installation
33 |
34 | ```
35 | npm install next-image-export-optimizer
36 |
37 | # Or
38 | yarn add next-image-export-optimizer
39 | pnpm install next-image-export-optimizer
40 | ```
41 |
42 | ## Configuration
43 |
44 | ## Basic configuration
45 |
46 | Add the following to your `next.config.js`. You can also use `next.config.ts` for TypeScript projects.:
47 |
48 | ```javascript
49 | // next.config.js
50 | module.exports = {
51 | output: "export",
52 | images: {
53 | loader: "custom",
54 | imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
55 | deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
56 | },
57 | transpilePackages: ["next-image-export-optimizer"],
58 | env: {
59 | nextImageExportOptimizer_imageFolderPath: "public/images",
60 | nextImageExportOptimizer_exportFolderPath: "out",
61 | nextImageExportOptimizer_quality: "75",
62 | nextImageExportOptimizer_storePicturesInWEBP: "true",
63 | nextImageExportOptimizer_exportFolderName: "nextImageExportOptimizer",
64 | nextImageExportOptimizer_generateAndUseBlurImages: "true",
65 | nextImageExportOptimizer_remoteImageCacheTTL: "0",
66 | },
67 | };
68 | ```
69 |
70 | Update the build command in `package.json`
71 |
72 | ```diff
73 | {
74 | - "build": "next build",
75 | + "build": "next build && next-image-export-optimizer"
76 | }
77 | ```
78 |
79 | Replace the **\** component with the **\** component:
80 |
81 | Example:
82 |
83 | ```javascript
84 | // Old
85 | import Image from "next/image";
86 |
87 | ;
93 |
94 | // Replace with either of the following:
95 |
96 | // With static import (Recommended)
97 | import ExportedImage from "next-image-export-optimizer";
98 | import testPictureStatic from "PATH_TO_IMAGE/test_static.jpg";
99 |
100 | ;
101 |
102 | // With dynamic import
103 | import ExportedImage from "next-image-export-optimizer";
104 |
105 | ;
111 | ```
112 |
113 | ## Advanced configuration
114 |
115 | ### Remote images
116 |
117 | For remote images, you have to specify the src as a string starting with either http or https in the ExportedImage component.
118 |
119 | ```javascript
120 | import ExportedImage from "next-image-export-optimizer";
121 |
122 | ;
123 | ```
124 |
125 | In order for the image optimization at build time to work correctly, you have to specify all remote image urls in a file called **remoteOptimizedImages.js** in the root directory of your project (where the `next.config.js` is stored as well). The file should export an array of strings containing the urls of the remote images. Returning a promise of such array is also supported.
126 |
127 | Example:
128 |
129 | ```javascript
130 | // remoteOptimizedImages.js
131 | module.exports = [
132 | "https://example.com/image1.jpg",
133 | "https://example.com/image2.jpg",
134 | "https://example.com/image3.jpg",
135 | // ...
136 | ];
137 | ```
138 |
139 | ```javascript
140 | // Or with a promise
141 | module.exports = new Promise((resolve) =>
142 | resolve([
143 | "https://example.com/image1.jpg",
144 | "https://example.com/image2.jpg",
145 | "https://example.com/image3.jpg",
146 | // ...
147 | ])
148 | );
149 |
150 | // Or with an async API call
151 | module.exports = fetch("https://example.com/api/images").catch((error) => {
152 | console.error(error);
153 | return []; // return an empty array in case of error
154 | });
155 | ```
156 |
157 | At build time, the images will be either downloaded or read from the cache. The image urls will be replaced with the optimized image urls in the Exported Image component.
158 |
159 | You can specify the time to live of the cache in seconds by setting the `nextImageExportOptimizer_remoteImageCacheTTL` environment variable in your `next.config.js` file. The default value is 0 seconds (as the image might have changed).
160 |
161 | Set it to:
162 |
163 | - 60 for 1 minute
164 | - 3600 for 1 hour
165 | - 86400 for 1 day
166 | - 604800 for 1 week
167 | - 2592000 for 1 month
168 | - 31536000 for 1 year
169 |
170 | If you want to hide the remote image urls from the user, you can use the [overrideSrc](https://nextjs.org/docs/pages/api-reference/components/image#overridesrc) prop of the ExportedImage component. This will replace the src attribute of the image tag with the value of the overrideSrc prop.
171 |
172 | Beware that the Image component cannot fall back to the original image URL if the optimized images are not yet generated when you use the overrideSrc prop. This will result in a broken image link.
173 |
174 | You can customize the filename for remote optimized images by adding the following to your `next.config.js`:
175 |
176 | ```javascript
177 | module.exports = {
178 | env: {
179 | // ... other env variables
180 | nextImageExportOptimizer_remoteImagesFilename: "remoteOptimizedImages.cjs",
181 | },
182 | // ... other config options
183 | };
184 | ```
185 |
186 | ### Custom next.config.js path
187 |
188 | If your Next.js project is not at the root directory where you are running the commands, for example when you are using a monorepo, you can specify the location of the `next.config.js` as an argument to the script:
189 |
190 | ```json
191 | "export": "next build && next-image-export-optimizer --nextConfigPath path/to/my/next.config.js"
192 | ```
193 |
194 | ### Custom export folder path
195 |
196 | Specify the output folder path either via environment variable:
197 |
198 | ```javascript
199 | // next.config.js
200 | { "env": {
201 | "nextImageExportOptimizer_exportFolderPath": "path/to/my/export/folder"
202 | }}
203 | ```
204 |
205 | Or by passing the argument to the script:
206 |
207 | ```json
208 | "export": "next build && next-image-export-optimizer --exportFolderPath path/to/my/export/folder"
209 | ```
210 |
211 | ### Base path
212 |
213 | If you want to deploy your app to a subfolder of your domain, you can set the basePath in the `next.config.js` file:
214 |
215 | ```javascript
216 | module.exports = {
217 | basePath: "/subfolder",
218 | };
219 | ```
220 |
221 | The ExportedImage component has a basePath prop which you can use to pass the basePath to the component.
222 |
223 | ```javascript
224 | import ExportedImage from "next-image-export-optimizer";
225 | import testPictureStatic from "PATH_TO_IMAGE/test_static.jpg";
226 |
227 | ;
232 | ```
233 |
234 | ### Placeholder images
235 |
236 | If you do not want the automatic generation of tiny, blurry placeholder images, set the `nextImageExportOptimizer_generateAndUseBlurImages` environment variable to `false` and set the `placeholder` prop from the **\** component to `empty`.
237 |
238 | ### Custom export folder name
239 |
240 | If you want to rename the export folder name, set the `nextImageExportOptimizer_exportFolderPath` environment variable to the desired folder name. The default is `nextImageExportOptimizer`.
241 |
242 | ### Image format
243 |
244 | By default, the images are stored in the WEBP format.
245 |
246 | If you do not want to use the WEBP format, set the `nextImageExportOptimizer_storePicturesInWEBP` environment variable to `false`.
247 |
248 | ## Good to know
249 |
250 | - The **\** component is a wrapper around the **\** component of Next.js. It uses the custom loader feature to generate a [srcset](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images) for different resolutions of the original image. The browser can then load the correct size based on the current viewport size.
251 |
252 | - The image transformation operation is optimized as it uses hashes to determine whether an image has already been optimized or not. This way, the images are only optimized once and not every time the build command is run.
253 |
254 | - The **\** component falls back to the original image if the optimized images are not yet generated in the development mode. In the exported, static React app, the responsive images are available as srcset and dynamically loaded by the browser.
255 |
256 | - The static import method is recommended as it informs the client about the original image size. When widths larger than the original image width are requested, the next largest image size in the deviceSizes array (specified in the `next.config.js`) will be used for the generation of the srcset attribute.
257 | When you specify the images as a path string, this library will create duplicates of the original image for each image size in the deviceSizes array that is larger than the original image size.
258 |
259 | - You can output the original, unoptimized images using the `unoptimized` prop.
260 | Example:
261 |
262 | ```javascript
263 | import ExportedImage from "next-image-export-optimizer";
264 |
265 | ;
270 | ```
271 |
272 | - You can still use the legacy image component `next/legacy/image`:
273 |
274 | ```javascript
275 | import ExportedImage from "next-image-export-optimizer/legacy/ExportedImage";
276 |
277 | import testPictureStatic from "PATH_TO_IMAGE/test_static.jpg";
278 |
279 | ;
280 | ```
281 |
282 | - Animated images:
283 | You can use .gif and animated .webp images. Next-image-export-optimizer will automatically optimize the animated images and generate the srcset for the different resolutions.
284 |
285 | If you set the variable nextImageExportOptimizer_storePicturesInWEBP to true, the animated images will be converted to .webp format which can reduce the file size significantly.
286 | Note that animated png images are not supported by this package.
287 |
288 | ## Live example
289 |
290 | You can see a live example of the use of this library at [reactapp.dev/next-image-export-optimizer](https://reactapp.dev/next-image-export-optimizer)
291 |
292 | > **Warning**
293 | > Version 1.0.0 is a breaking change. It follows the changes introduced in Next 13.0.0 which replaces the `next/image` component with `next/future/image`. If you are using Next 12 or below, please use version _0.17.1_.
294 |
--------------------------------------------------------------------------------
/environment.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | namespace NodeJS {
3 | interface ProcessEnv {
4 | storePicturesInWEBP: string | undefined;
5 | generateAndUseBlurImages: string | undefined;
6 | nextImageExportOptimizer_storePicturesInWEBP: string | undefined;
7 | nextImageExportOptimizer_generateAndUseBlurImages: string | undefined;
8 | nextImageExportOptimizer_exportFolderName: string | undefined;
9 | nextImageExportOptimizer_quality: string | undefined;
10 | nextImageExportOptimizer_remoteImageCacheTTL: string | undefined;
11 | __NEXT_IMAGE_OPTS: { deviceSizes: string[]; imageSizes: string[] };
12 | }
13 | }
14 | }
15 |
16 | // If this file has no import/export statements (i.e. is a script)
17 | // convert it into a module by adding an empty export statement.
18 | export {};
19 |
--------------------------------------------------------------------------------
/example/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | public/images/nextImageExportOptimizer
37 | remoteImagesForOptimization
38 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project to test the next-image-export-optimizer module.
2 |
--------------------------------------------------------------------------------
/example/app/appdir/page.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 6rem;
7 | min-height: 100vh;
8 | }
9 |
10 | .description {
11 | display: inherit;
12 | justify-content: inherit;
13 | align-items: inherit;
14 | font-size: 0.85rem;
15 | max-width: var(--max-width);
16 | width: 100%;
17 | z-index: 2;
18 | font-family: var(--font-mono);
19 | }
20 |
21 | .description a {
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | gap: 0.5rem;
26 | }
27 |
28 | .description p {
29 | position: relative;
30 | margin: 0;
31 | padding: 1rem;
32 | background-color: rgba(var(--callout-rgb), 0.5);
33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3);
34 | border-radius: var(--border-radius);
35 | }
36 |
37 | .code {
38 | font-weight: 700;
39 | font-family: var(--font-mono);
40 | }
41 |
42 | .grid {
43 | display: grid;
44 | grid-template-columns: repeat(4, minmax(25%, auto));
45 | width: var(--max-width);
46 | max-width: 100%;
47 | }
48 |
49 | .card {
50 | padding: 1rem 1.2rem;
51 | border-radius: var(--border-radius);
52 | background: rgba(var(--card-rgb), 0);
53 | border: 1px solid rgba(var(--card-border-rgb), 0);
54 | transition: background 200ms, border 200ms;
55 | }
56 |
57 | .card span {
58 | display: inline-block;
59 | transition: transform 200ms;
60 | }
61 |
62 | .card h2 {
63 | font-weight: 600;
64 | margin-bottom: 0.7rem;
65 | }
66 |
67 | .card p {
68 | margin: 0;
69 | opacity: 0.6;
70 | font-size: 0.9rem;
71 | line-height: 1.5;
72 | max-width: 30ch;
73 | }
74 |
75 | .center {
76 | display: flex;
77 | justify-content: center;
78 | align-items: center;
79 | position: relative;
80 | padding: 4rem 0;
81 | }
82 |
83 | .center::before {
84 | background: var(--secondary-glow);
85 | border-radius: 50%;
86 | width: 480px;
87 | height: 360px;
88 | margin-left: -400px;
89 | }
90 |
91 | .center::after {
92 | background: var(--primary-glow);
93 | width: 240px;
94 | height: 180px;
95 | z-index: -1;
96 | }
97 |
98 | .center::before,
99 | .center::after {
100 | content: '';
101 | left: 50%;
102 | position: absolute;
103 | filter: blur(45px);
104 | transform: translateZ(0);
105 | }
106 |
107 | .logo {
108 | position: relative;
109 | }
110 | /* Enable hover only on non-touch devices */
111 | @media (hover: hover) and (pointer: fine) {
112 | .card:hover {
113 | background: rgba(var(--card-rgb), 0.1);
114 | border: 1px solid rgba(var(--card-border-rgb), 0.15);
115 | }
116 |
117 | .card:hover span {
118 | transform: translateX(4px);
119 | }
120 | }
121 |
122 | @media (prefers-reduced-motion) {
123 | .card:hover span {
124 | transform: none;
125 | }
126 | }
127 |
128 | /* Mobile */
129 | @media (max-width: 700px) {
130 | .content {
131 | padding: 4rem;
132 | }
133 |
134 | .grid {
135 | grid-template-columns: 1fr;
136 | margin-bottom: 120px;
137 | max-width: 320px;
138 | text-align: center;
139 | }
140 |
141 | .card {
142 | padding: 1rem 2.5rem;
143 | }
144 |
145 | .card h2 {
146 | margin-bottom: 0.5rem;
147 | }
148 |
149 | .center {
150 | padding: 8rem 0 6rem;
151 | }
152 |
153 | .center::before {
154 | transform: none;
155 | height: 300px;
156 | }
157 |
158 | .description {
159 | font-size: 0.8rem;
160 | }
161 |
162 | .description a {
163 | padding: 1rem;
164 | }
165 |
166 | .description p,
167 | .description div {
168 | display: flex;
169 | justify-content: center;
170 | position: fixed;
171 | width: 100%;
172 | }
173 |
174 | .description p {
175 | align-items: center;
176 | inset: 0 0 auto;
177 | padding: 2rem 1rem 1.4rem;
178 | border-radius: 0;
179 | border: none;
180 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
181 | background: linear-gradient(
182 | to bottom,
183 | rgba(var(--background-start-rgb), 1),
184 | rgba(var(--callout-rgb), 0.5)
185 | );
186 | background-clip: padding-box;
187 | backdrop-filter: blur(24px);
188 | }
189 |
190 | .description div {
191 | align-items: flex-end;
192 | pointer-events: none;
193 | inset: auto 0 0;
194 | padding: 2rem;
195 | height: 200px;
196 | background: linear-gradient(
197 | to bottom,
198 | transparent 0%,
199 | rgb(var(--background-end-rgb)) 40%
200 | );
201 | z-index: 1;
202 | }
203 | }
204 |
205 | /* Tablet and Smaller Desktop */
206 | @media (min-width: 701px) and (max-width: 1120px) {
207 | .grid {
208 | grid-template-columns: repeat(2, 50%);
209 | }
210 | }
211 |
212 | @media (prefers-color-scheme: dark) {
213 | .vercelLogo {
214 | filter: invert(1);
215 | }
216 |
217 | .logo {
218 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
219 | }
220 | }
221 |
222 | @keyframes rotate {
223 | from {
224 | transform: rotate(360deg);
225 | }
226 | to {
227 | transform: rotate(0deg);
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/example/app/appdir/page.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../../styles/Home.module.css";
2 | import ExportedImage from "../../src/ExportedImage";
3 | import ExportedImageLegacy from "../../src/legacy/ExportedImage";
4 | import testPictureStatic from "../../public/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.jpg";
5 |
6 | export default function Home() {
7 | // get the basePath set in next.config.js
8 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
9 | return (
10 |
11 | Next-Image-Export-Optimizer
12 | Optimized example - Legacy
13 |
21 |
30 |
31 | Optimized example (static import) - Legacy
32 |
39 |
47 |
48 | Optimized example
49 |
54 |
62 |
63 | Optimized example (fill)
64 |
72 |
80 |
81 | Optimized example (fill & static import)
82 |
90 |
98 |
99 | Optimized example (static import)
100 |
107 |
116 |
117 | Unoptimized example - Legacy
118 |
126 |
136 |
137 | Unoptimized example - Legacy static import
138 |
146 |
156 |
157 | Unoptimized example
158 |
166 |
176 |
177 | Unoptimized example static import
178 |
186 |
196 |
197 | {/* */}
204 |
205 | );
206 | }
207 |
--------------------------------------------------------------------------------
/example/app/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
--------------------------------------------------------------------------------
/example/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 | import { Inter } from "next/font/google";
3 | import React from "react";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata = {
8 | title: "Create Next App",
9 | description: "Generated by create next app",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: {
15 | children: React.ReactNode;
16 | }) {
17 | return (
18 |
19 |
{children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/example/assets/chris-zhang-Jq8-3Bmh1pQ-unsplash_static_asset.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Niels-IO/next-image-export-optimizer/32dec4ab1aba126450b45b5e4b3622c8731fea19/example/assets/chris-zhang-Jq8-3Bmh1pQ-unsplash_static_asset.jpg
--------------------------------------------------------------------------------
/example/environment.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | namespace NodeJS {
3 | interface ProcessEnv {
4 | storePicturesInWEBP: string | undefined;
5 | generateAndUseBlurImages: string | undefined;
6 | nextImageExportOptimizer_storePicturesInWEBP: string | undefined;
7 | nextImageExportOptimizer_generateAndUseBlurImages: string | undefined;
8 | nextImageExportOptimizer_exportFolderName: string | undefined;
9 | nextImageExportOptimizer_quality: string | undefined;
10 | __NEXT_IMAGE_OPTS: { deviceSizes: string[]; imageSizes: string[] };
11 | }
12 | }
13 | }
14 |
15 | // If this file has no import/export statements (i.e. is a script)
16 | // convert it into a module by adding an empty export statement.
17 | export {};
18 |
--------------------------------------------------------------------------------
/example/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
7 |
--------------------------------------------------------------------------------
/example/next.config.ts:
--------------------------------------------------------------------------------
1 |
2 | import type { NextConfig } from 'next'
3 |
4 | const nextConfig: NextConfig = {
5 | images: {
6 | loader: "custom",
7 | imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
8 | deviceSizes: [640, 750, 777, 828, 1080, 1200, 1920, 2048, 3840],
9 | },
10 | output: "export",
11 | transpilePackages: ["next-image-export-optimizer"],
12 | env: {
13 | nextImageExportOptimizer_imageFolderPath: "public/images",
14 | nextImageExportOptimizer_exportFolderPath: "out",
15 | nextImageExportOptimizer_exportFolderName: "nextImageExportOptimizer",
16 | nextImageExportOptimizer_quality: "75",
17 | nextImageExportOptimizer_storePicturesInWEBP: "true",
18 | nextImageExportOptimizer_generateAndUseBlurImages: "true",
19 | },
20 | };
21 | export default nextConfig
22 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "private": true,
4 | "scripts": {
5 | "dev": "next dev",
6 | "build": "next build",
7 | "export": "next build && cd .. && npm run build && cd example && node ../dist/optimizeImages.js",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "next": "^15.0.3",
13 | "next-image-export-optimizer": "^1.17.0-beta.3",
14 | "react": "19.0.0-rc-66855b96-20241106",
15 | "react-dom": "19.0.0-rc-66855b96-20241106"
16 | },
17 | "devDependencies": {
18 | "@types/node": "^22.9.0",
19 | "@types/react": "npm:types-react@rc",
20 | "eslint-config-next": "^15.0.3",
21 | "express": "^4.18.2"
22 | },
23 | "overrides": {
24 | "@types/react": "npm:types-react@rc",
25 | "@types/react-dom": "npm:types-react-dom@rc"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/example/pages/_app.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import "../styles/globals.css";
3 |
4 | function MyApp({ Component, pageProps }) {
5 | return ;
6 | }
7 |
8 | export default MyApp;
9 |
--------------------------------------------------------------------------------
/example/pages/fixedImage.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import ExportedImageLegacy from "../src/legacy/ExportedImage";
3 | import ExportedImage from "../src/ExportedImage";
4 | // import ExportedImage from "next-image-export-optimizer";
5 |
6 | import styles from "../styles/Home.module.css";
7 | import testPictureStatic from "../assets/chris-zhang-Jq8-3Bmh1pQ-unsplash_static_asset.jpg";
8 |
9 | export default function Home() {
10 | // get the basePath set in next.config.js
11 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
12 | return (
13 |
14 |
15 |
Next-Image-Export-Optimizer
16 |
20 |
21 |
22 |
23 |
24 | Next-Image-Export-Optimizer
25 | Fixed size test page
26 |
34 | {[16, 32, 48, 64, 96, 128, 256, 384].map((size) => (
35 |
36 |
47 |
57 |
67 |
68 | ))}
69 |
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/example/pages/forwardRef.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import ExportedImage from "../src/ExportedImage";
3 |
4 | import styles from "../styles/Home.module.css";
5 | import { useEffect, useRef, useState } from "react";
6 |
7 | export default function Home() {
8 | // example of using a ref to get the image dimensions to test the forwardRef functionality
9 | const imageRef = useRef(null);
10 | const [imageDimensions, setImageDimensions] = useState({
11 | width: 0,
12 | height: 0,
13 | });
14 | useEffect(() => {
15 | if (imageRef.current) {
16 | setImageDimensions({
17 | width: imageRef.current.clientWidth,
18 | height: imageRef.current.clientHeight,
19 | });
20 | }
21 | }, [imageRef]);
22 | // get the basePath set in next.config.js
23 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
24 |
25 | return (
26 |
27 |
28 |
Next-Image-Export-Optimizer
29 |
33 |
34 |
35 |
36 |
37 | Next-Image-Export-Optimizer
38 | Optimized example
39 |
46 |
55 |
56 |
65 |
Width:
66 |
{imageDimensions.width}
67 |
Height:
68 |
{imageDimensions.height}
69 |
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/example/pages/gifs.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import ExportedImage from "../src/ExportedImage";
3 |
4 | import styles from "../styles/Home.module.css";
5 | import animatedImage from "../public/animated.png";
6 |
7 | export default function Home() {
8 | // get the basePath set in next.config.js
9 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
10 | return (
11 |
12 |
13 |
Next-Image-Export-Optimizer
14 |
18 |
19 |
20 |
21 |
22 | Next-Image-Export-Optimizer
23 | Format: .gif
24 |
25 |
33 |
41 |
42 | Format: .webp
43 |
44 |
52 |
60 |
61 | Format: .png
62 |
63 |
71 |
78 |
79 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/example/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import ExportedImageLegacy from "../src/legacy/ExportedImage";
3 | // import ExportedImageLegacy from "next-image-export-optimizer/legacy/ExportedImage";
4 | // import ExportedImage from "next-image-export-optimizer";
5 | import ExportedImage from "../src/ExportedImage";
6 |
7 | import styles from "../styles/Home.module.css";
8 | import testPictureStatic from "../public/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.jpg";
9 |
10 | export default function Home() {
11 | // get the basePath set in next.config.js
12 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
13 | return (
14 |
15 |
16 |
Next-Image-Export-Optimizer
17 |
21 |
22 |
23 |
24 |
25 | Next-Image-Export-Optimizer
26 | Optimized example - Legacy
27 |
35 |
44 |
45 | Optimized example (static import) - Legacy
46 |
53 |
61 |
62 | Optimized example
63 |
68 |
76 |
77 | Optimized example (fill)
78 |
86 |
95 |
96 | Optimized example (fill & static import)
97 |
105 |
113 |
114 | Optimized example (static import)
115 |
122 |
132 |
133 | Unoptimized example - Legacy
134 |
142 |
152 |
153 | Unoptimized example - Legacy static import
154 |
162 |
172 |
173 | Unoptimized example
174 |
182 |
192 |
193 | Unoptimized example static import
194 |
202 |
212 |
213 | {/* */}
220 |
221 |
222 | );
223 | }
224 |
--------------------------------------------------------------------------------
/example/pages/nested/page.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ExportedImage from "../../src/ExportedImage";
3 | import ExportedImageLegacy from "../../src/legacy/ExportedImage";
4 | import testPictureStatic from "../../public/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.jpg";
5 |
6 | function Page() {
7 | // get the basePath set in next.config.js
8 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
9 | return (
10 |
11 |
Nested page test
12 |
Optimized example (static import) - Legacy
13 |
14 |
21 |
28 |
29 |
Optimized example with fixed size (static import) - Legacy
30 |
31 |
38 |
47 |
48 |
Optimized example - Legacy
49 |
57 |
65 |
66 |
Optimized example
67 |
75 |
84 |
85 |
Optimized example (static import)
86 |
87 |
94 |
103 |
104 |
105 | );
106 | }
107 |
108 | export default Page;
109 |
--------------------------------------------------------------------------------
/example/pages/nested/page_fixed.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ExportedImage from "../../src/ExportedImage";
3 | import ExportedImageLegacy from "../../src/legacy/ExportedImage";
4 | import testPictureStatic from "../../public/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.jpg";
5 |
6 | function Page() {
7 | // get the basePath set in next.config.js
8 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
9 | return (
10 |
11 |
Nested page test
12 |
13 |
Optimized example with fixed size (static import) - Legacy
14 |
15 |
22 |
31 |
32 |
Optimized example with fixed size (static import)
33 |
34 |
41 |
50 |
51 |
52 | );
53 | }
54 |
55 | export default Page;
56 |
--------------------------------------------------------------------------------
/example/pages/nestedSlug/[slug].js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ExportedImage from "../../src/ExportedImage";
3 | import ExportedImageLegacy from "../../src/legacy/ExportedImage";
4 | import testPictureStatic from "../../public/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.jpg";
5 |
6 | function Slug() {
7 | // get the basePath set in next.config.js
8 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
9 | return (
10 |
11 |
Nested slug page test
12 |
Optimized example (static import) - Legacy
13 |
14 |
21 |
28 |
29 |
Optimized example (fill)
30 |
38 |
46 |
47 |
Optimized example (fill & static import)
48 |
56 |
64 |
65 |
66 | );
67 | }
68 |
69 | export async function getStaticPaths() {
70 | return {
71 | paths: [{ params: { slug: "page" } }],
72 | fallback: false, // can also be true or 'blocking'
73 | };
74 | }
75 |
76 | // `getStaticPaths` requires using `getStaticProps`
77 | export async function getStaticProps() {
78 | return {
79 | // Passed to the page component as props
80 | props: { post: {} },
81 | };
82 | }
83 |
84 | export default Slug;
85 |
--------------------------------------------------------------------------------
/example/pages/remote.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import ExportedImage from "../src/ExportedImage";
3 |
4 | import styles from "../styles/Home.module.css";
5 |
6 | export default function Home() {
7 | // get the basePath set in next.config.js
8 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
9 | return (
10 |
11 |
12 |
Next-Image-Export-Optimizer
13 |
17 |
18 |
19 |
20 |
21 | Next-Image-Export-Optimizer
22 | Optimized example - Remote
23 |
31 |
40 |
41 |
49 |
59 |
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/example/pages/smallImage.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import ExportedImageLegacy from "../src/legacy/ExportedImage";
3 | import ExportedImage from "../src/ExportedImage";
4 |
5 | import smallImage from "../public/images/chris-zhang-Jq8-3Bmh1pQ-unsplash_small.jpg";
6 |
7 | import styles from "../styles/Home.module.css";
8 |
9 | export default function Home() {
10 | // get the basePath set in next.config.js
11 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
12 | return (
13 |
14 |
15 |
Next-Image-Export-Optimizer
16 |
20 |
21 |
22 |
23 |
24 | Next-Image-Export-Optimizer
25 | Optimized example - Legacy
26 |
34 |
43 |
44 | Optimized example
45 |
46 |
53 |
62 |
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/example/pages/subfolder.js:
--------------------------------------------------------------------------------
1 | import ExportedImageLegacy from "../src/legacy/ExportedImage";
2 | import Head from "next/head";
3 | import React from "react";
4 | import styles from "../styles/Home.module.css";
5 |
6 | function Subfolder() {
7 | // get the basePath set in next.config.js
8 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
9 | return (
10 |
11 |
12 |
Next-Image-Export-Optimizer
13 |
17 |
18 |
19 |
20 |
21 | Subfolder test
22 |
30 |
39 |
40 |
48 |
57 |
58 |
59 |
60 | );
61 | }
62 |
63 | export default Subfolder;
64 |
--------------------------------------------------------------------------------
/example/pages/transparent.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import ExportedImage from "../src/ExportedImage";
3 |
4 | import styles from "../styles/Home.module.css";
5 |
6 | export default function Home() {
7 | // get the basePath set in next.config.js
8 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
9 | return (
10 |
11 |
12 |
Next-Image-Export-Optimizer
13 |
17 |
18 |
19 |
20 |
21 | Next-Image-Export-Optimizer
22 | Optimized example
23 |
30 |
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/example/pages/typescript.tsx:
--------------------------------------------------------------------------------
1 | import ExportedImage from "next-image-export-optimizer";
2 | import ExportedImageLegacy from "next-image-export-optimizer/legacy/ExportedImage";
3 | import Head from "next/head";
4 | import ExportedImage_Local from "../src/ExportedImage";
5 | import ExportedImageLegacy_Local from "../src/legacy/ExportedImage";
6 |
7 | import Image from "next/image";
8 | import testPictureStatic from "../public/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.jpg";
9 |
10 | export default function Home() {
11 | // get the basePath set in next.config.js
12 | const basePath = process.env.__NEXT_ROUTER_BASEPATH || "";
13 | return (
14 |
15 |
16 |
Next-Image-Export-Optimizer
17 |
21 |
22 |
23 |
24 |
25 | Next-Image-Export-Optimizer
26 |
34 |
43 |
44 |
52 |
63 |
64 |
72 |
83 |
84 |
85 |
92 |
99 |
100 |
107 |
114 |
115 |
121 |
128 |
135 | {
138 | return src;
139 | }}
140 | width={400}
141 | height={400}
142 | alt="SVG"
143 | />
144 |
145 |
146 |
147 | );
148 | }
149 |
--------------------------------------------------------------------------------
/example/public/animated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Niels-IO/next-image-export-optimizer/32dec4ab1aba126450b45b5e4b3622c8731fea19/example/public/animated.png
--------------------------------------------------------------------------------
/example/public/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Niels-IO/next-image-export-optimizer/32dec4ab1aba126450b45b5e4b3622c8731fea19/example/public/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.jpg
--------------------------------------------------------------------------------
/example/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Niels-IO/next-image-export-optimizer/32dec4ab1aba126450b45b5e4b3622c8731fea19/example/public/favicon.ico
--------------------------------------------------------------------------------
/example/public/images/402107790_STATIC_NOISE_GIF.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Niels-IO/next-image-export-optimizer/32dec4ab1aba126450b45b5e4b3622c8731fea19/example/public/images/402107790_STATIC_NOISE_GIF.gif
--------------------------------------------------------------------------------
/example/public/images/402107790_STATIC_NOISE_WEBP.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Niels-IO/next-image-export-optimizer/32dec4ab1aba126450b45b5e4b3622c8731fea19/example/public/images/402107790_STATIC_NOISE_WEBP.webp
--------------------------------------------------------------------------------
/example/public/images/chris-zhang-Jq8-3Bmh1pQ-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Niels-IO/next-image-export-optimizer/32dec4ab1aba126450b45b5e4b3622c8731fea19/example/public/images/chris-zhang-Jq8-3Bmh1pQ-unsplash.jpg
--------------------------------------------------------------------------------
/example/public/images/chris-zhang-Jq8-3Bmh1pQ-unsplash_small.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Niels-IO/next-image-export-optimizer/32dec4ab1aba126450b45b5e4b3622c8731fea19/example/public/images/chris-zhang-Jq8-3Bmh1pQ-unsplash_small.jpg
--------------------------------------------------------------------------------
/example/public/images/next-image-export-optimizer-hashes.json:
--------------------------------------------------------------------------------
1 | {
2 | "/402107790_STATIC_NOISE_GIF.gif": "QJxjefkpS5FMjtaQKuEdNTEpqunW3A+dIKHFl2ImZR0=",
3 | "/402107790_STATIC_NOISE_WEBP.webp": "uZRLAraNeZHDwCPNJdTdyVlkcPdWrLp-I4AW5MSYBbw=",
4 | "/chris-zhang-Jq8-3Bmh1pQ-unsplash.jpg": "HzEIrObHWZKrBNj0BsxsjyHTujdcALWCgDC-sH+nRaM=",
5 | "/chris-zhang-Jq8-3Bmh1pQ-unsplash_small.jpg": "uaQKauUmJn2erNY1qBjQNi-Ky+yipqW7BJebV8DCYHI=",
6 | "subfolder/ollie-barker-jones-K52HVSPVvKI-unsplash.jpg": "eCN-BKBwsstx+QGPEOXBodpbU1DUWgpf1DhnuXeoG8w=",
7 | "subfolder/subfolder2/ollie-barker-jones-K52HVSPVvKI-unsplash.jpg": "AWW05lFZl-Qt-8gAeuDu3bnMm9m6lyTUH80yXpit0Og=",
8 | "/transparentImage.png": "XH3+oYb10y7DOx5iqAbzi64ChyebfrxLgJNJolxjpPw=",
9 | "/animated.c00e0188.png": "1u18UQP7SYClRgh+v8TnzU82uY96MSKW3bxNat8HYOo=",
10 | "/chris-zhang-Jq8-3Bmh1pQ-unsplash_small.0fa13b23.jpg": "w8j9FhKoGEyo52uc8zEMt7XCeMUZsGCQjEjnWzkams4=",
11 | "/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.921260e0.jpg": "F4KuoW3LZSTHxrqqDmnFlIcTPSHwtJTKuB2djCCjEnw=",
12 | "/chris-zhang-Jq8-3Bmh1pQ-unsplash_static_asset.921260e0.jpg": "TKroa8LFPMSjLnRq67yFY71qpejsNlpVOV7TkeASSGA=",
13 | "/5206242668571649.WEBP": "NBLWVfuacpA+Xj8Z3PMTT8Q8UuFo9Dn2KHG6uMAGcJw=",
14 | "/6725071117443837.WEBP": "w5l7YQrMfohCxKHUQ+Z6ml2LJN36eH4gzJ0y6YWuYBU="
15 | }
--------------------------------------------------------------------------------
/example/public/images/subfolder/ollie-barker-jones-K52HVSPVvKI-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Niels-IO/next-image-export-optimizer/32dec4ab1aba126450b45b5e4b3622c8731fea19/example/public/images/subfolder/ollie-barker-jones-K52HVSPVvKI-unsplash.jpg
--------------------------------------------------------------------------------
/example/public/images/subfolder/subfolder2/ollie-barker-jones-K52HVSPVvKI-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Niels-IO/next-image-export-optimizer/32dec4ab1aba126450b45b5e4b3622c8731fea19/example/public/images/subfolder/subfolder2/ollie-barker-jones-K52HVSPVvKI-unsplash.jpg
--------------------------------------------------------------------------------
/example/public/images/transparentImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Niels-IO/next-image-export-optimizer/32dec4ab1aba126450b45b5e4b3622c8731fea19/example/public/images/transparentImage.png
--------------------------------------------------------------------------------
/example/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/remoteOptimizedImages.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | "https://reactapp.dev/images/nextImageExportOptimizer/christopher-gower-m_HRfLhgABo-unsplash-opt-2048.WEBP",
3 | "https://reactapp.dev/images/nextImageExportOptimizer/christopher-gower-m_HRfLhgABo-unsplash-opt-2048.WEBP?ref=next-image-export-optimizer",
4 | // 'https://example.com/image1.jpg',
5 | // 'https://example.com/image2.jpg',
6 | // 'https://example.com/image3.jpg',
7 | // ...
8 | ];
9 |
--------------------------------------------------------------------------------
/example/src/ExportedImage.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image, { ImageProps, StaticImageData } from "next/image";
4 | import React, { forwardRef, useCallback, useMemo, useState } from "react";
5 |
6 | const splitFilePath = ({ filePath }: { filePath: string }) => {
7 | const filenameWithExtension =
8 | filePath.split("\\").pop()?.split("/").pop() || "";
9 | const filePathWithoutFilename = filePath.split(filenameWithExtension).shift();
10 | const fileExtension = filePath.split(".").pop();
11 | const filenameWithoutExtension =
12 | filenameWithExtension.substring(
13 | 0,
14 | filenameWithExtension.lastIndexOf(".")
15 | ) || filenameWithExtension;
16 | return {
17 | path: filePathWithoutFilename,
18 | filename: filenameWithoutExtension,
19 | extension: fileExtension || "",
20 | };
21 | };
22 |
23 | const generateImageURL = (
24 | src: string,
25 | width: number,
26 | basePath: string | undefined,
27 | isRemoteImage: boolean = false
28 | ) => {
29 | const { filename, path, extension } = splitFilePath({ filePath: src });
30 | const useWebp =
31 | process.env.nextImageExportOptimizer_storePicturesInWEBP != undefined
32 | ? process.env.nextImageExportOptimizer_storePicturesInWEBP == "true"
33 | : true;
34 |
35 | if (
36 | !["JPG", "JPEG", "WEBP", "PNG", "AVIF", "GIF"].includes(
37 | extension.toUpperCase()
38 | )
39 | ) {
40 | // The images has an unsupported extension
41 | // We will return the src
42 | return src;
43 | }
44 | // If the images are stored as WEBP by the package, then we should change
45 | // the extension to WEBP to load them correctly
46 | let processedExtension = extension;
47 |
48 | if (
49 | useWebp &&
50 | ["JPG", "JPEG", "PNG", "GIF"].includes(extension.toUpperCase())
51 | ) {
52 | processedExtension = "WEBP";
53 | }
54 |
55 | let correctedPath = path;
56 | const lastChar = correctedPath?.substr(-1); // Selects the last character
57 | if (lastChar != "/") {
58 | // If the last character is not a slash
59 | correctedPath = correctedPath + "/"; // Append a slash to it.
60 | }
61 |
62 | const isStaticImage = src.includes("_next/static/media");
63 |
64 | if (basePath) {
65 | if (
66 | basePath.endsWith("/") &&
67 | correctedPath &&
68 | correctedPath.startsWith("/")
69 | ) {
70 | correctedPath = basePath + correctedPath.slice(1);
71 | } else if (
72 | !basePath.endsWith("/") &&
73 | correctedPath &&
74 | !correctedPath.startsWith("/")
75 | ) {
76 | correctedPath = basePath + "/" + correctedPath;
77 | } else {
78 | correctedPath = basePath + correctedPath;
79 | }
80 | }
81 |
82 | const exportFolderName =
83 | process.env.nextImageExportOptimizer_exportFolderName ||
84 | "nextImageExportOptimizer";
85 | const basePathPrefixForStaticImages = basePath ? basePath + "/" : "";
86 |
87 | let generatedImageURL = `${
88 | isStaticImage ? basePathPrefixForStaticImages : correctedPath
89 | }${exportFolderName}/${filename}-opt-${width}.${processedExtension.toUpperCase()}`;
90 |
91 | // if the generatedImageURL is not starting with a slash, then we add one as long as it is not a remote image
92 | if (!isRemoteImage && generatedImageURL.charAt(0) !== "/") {
93 | generatedImageURL = "/" + generatedImageURL;
94 | }
95 |
96 | return generatedImageURL;
97 | };
98 |
99 | // Credits to https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
100 | // This is a hash function that is used to generate a hash from the image URL
101 | const hashAlgorithm = (str: string, seed = 0) => {
102 | let h1 = 0xdeadbeef ^ seed,
103 | h2 = 0x41c6ce57 ^ seed;
104 | for (let i = 0, ch; i < str.length; i++) {
105 | ch = str.charCodeAt(i);
106 | h1 = Math.imul(h1 ^ ch, 2654435761);
107 | h2 = Math.imul(h2 ^ ch, 1597334677);
108 | }
109 | h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
110 | h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
111 | h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
112 | h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
113 |
114 | return 4294967296 * (2097151 & h2) + (h1 >>> 0);
115 | };
116 |
117 | function urlToFilename(url: string) {
118 | try {
119 | const parsedUrl = new URL(url);
120 | const extension = parsedUrl.pathname.split(".").pop();
121 | if (extension) {
122 | return hashAlgorithm(url).toString().concat(".", extension);
123 | }
124 | } catch (error) {
125 | console.error("Error parsing URL", url, error);
126 | }
127 | return hashAlgorithm(url).toString();
128 | }
129 |
130 | const imageURLForRemoteImage = ({
131 | src,
132 | width,
133 | basePath,
134 | }: {
135 | src: string;
136 | width: number;
137 | basePath: string | undefined;
138 | }) => {
139 | const encodedSrc = urlToFilename(src);
140 |
141 | return generateImageURL(encodedSrc, width, basePath, true);
142 | };
143 |
144 | const optimizedLoader = ({
145 | src,
146 | width,
147 | basePath,
148 | }: {
149 | src: string | StaticImageData;
150 | width: number;
151 | basePath: string | undefined;
152 | }) => {
153 | const isStaticImage = typeof src === "object";
154 | const _src = isStaticImage ? src.src : src;
155 | const originalImageWidth = (isStaticImage && src.width) || undefined;
156 |
157 | // if it is a static image, we can use the width of the original image to generate a reduced srcset that returns
158 | // the same image url for widths that are larger than the original image
159 | if (isStaticImage && originalImageWidth && width > originalImageWidth) {
160 | const deviceSizes = (
161 | process.env.__NEXT_IMAGE_OPTS?.deviceSizes || [
162 | 640, 750, 828, 1080, 1200, 1920, 2048, 3840,
163 | ]
164 | ).map(Number);
165 | const imageSizes = (
166 | process.env.__NEXT_IMAGE_OPTS?.imageSizes || [
167 | 16, 32, 48, 64, 96, 128, 256, 384,
168 | ]
169 | ).map(Number);
170 | let allSizes: number[] = [...deviceSizes, ...imageSizes];
171 | allSizes = allSizes.filter((v, i, a) => a.indexOf(v) === i);
172 | allSizes.sort((a, b) => a - b);
173 |
174 | // only use the width if it is smaller or equal to the next size in the allSizes array
175 | let nextLargestSize = null;
176 | for (let i = 0; i < allSizes.length; i++) {
177 | if (
178 | Number(allSizes[i]) >= originalImageWidth &&
179 | (nextLargestSize === null || Number(allSizes[i]) < nextLargestSize)
180 | ) {
181 | nextLargestSize = Number(allSizes[i]);
182 | }
183 | }
184 |
185 | if (nextLargestSize !== null) {
186 | return generateImageURL(_src, nextLargestSize, basePath);
187 | }
188 | }
189 |
190 | // Check if the image is a remote image (starts with http or https)
191 | if (_src.startsWith("http")) {
192 | return imageURLForRemoteImage({ src: _src, width, basePath });
193 | }
194 |
195 | return generateImageURL(_src, width, basePath);
196 | };
197 |
198 | const fallbackLoader = ({ src }: { src: string | StaticImageData }) => {
199 | let _src = typeof src === "object" ? src.src : src;
200 |
201 | const isRemoteImage = _src.startsWith("http");
202 |
203 | // if the _src does not start with a slash, then we add one as long as it is not a remote image
204 | if (!isRemoteImage && _src.charAt(0) !== "/") {
205 | _src = "/" + _src;
206 | }
207 | return _src;
208 | };
209 |
210 | export interface ExportedImageProps
211 | extends Omit {
212 | src: string | StaticImageData;
213 | basePath?: string;
214 | }
215 |
216 | const ExportedImage = forwardRef(
217 | (
218 | {
219 | src,
220 | priority = false,
221 | loading,
222 | className,
223 | width,
224 | height,
225 | onLoad,
226 | unoptimized,
227 | placeholder = "blur",
228 | basePath = "",
229 | alt = "",
230 | blurDataURL,
231 | style,
232 | onError,
233 | overrideSrc,
234 | ...rest
235 | },
236 | ref
237 | ) => {
238 | const [imageError, setImageError] = useState(false);
239 | const automaticallyCalculatedBlurDataURL = useMemo(() => {
240 | if (blurDataURL) {
241 | // use the user provided blurDataURL if present
242 | return blurDataURL;
243 | }
244 | // check if the src is specified as a local file -> then it is an object
245 | const isStaticImage = typeof src === "object";
246 | let _src = isStaticImage ? src.src : src;
247 |
248 | if (unoptimized === true) {
249 | // return the src image when unoptimized
250 | return _src;
251 | }
252 | // Check if the image is a remote image (starts with http or https)
253 | if (_src.startsWith("http")) {
254 | return imageURLForRemoteImage({ src: _src, width: 10, basePath });
255 | }
256 |
257 | // otherwise use the generated image of 10px width as a blurDataURL
258 | return generateImageURL(_src, 10, basePath);
259 | }, [blurDataURL, src, unoptimized, basePath]);
260 |
261 | // check if the src is a SVG image -> then we should not use the blurDataURL and use unoptimized
262 | const isSVG =
263 | typeof src === "object" ? src.src.endsWith(".svg") : src.endsWith(".svg");
264 |
265 | const [blurComplete, setBlurComplete] = useState(false);
266 |
267 | // Currently, we have to handle the blurDataURL ourselves as the new Image component
268 | // is expecting a base64 encoded string, but the generated blurDataURL is a normal URL
269 | const blurStyle =
270 | placeholder === "blur" &&
271 | !isSVG &&
272 | automaticallyCalculatedBlurDataURL &&
273 | automaticallyCalculatedBlurDataURL.startsWith("/") &&
274 | !blurComplete
275 | ? {
276 | backgroundSize: style?.objectFit || "cover",
277 | backgroundPosition: style?.objectPosition || "50% 50%",
278 | backgroundRepeat: "no-repeat",
279 | backgroundImage: `url("${automaticallyCalculatedBlurDataURL}")`,
280 | }
281 | : undefined;
282 | const isStaticImage = typeof src === "object";
283 |
284 | let _src = isStaticImage ? src.src : src;
285 | if (basePath && !isStaticImage && _src.startsWith("/")) {
286 | _src = basePath + _src;
287 | }
288 | if (basePath && !isStaticImage && !_src.startsWith("/")) {
289 | _src = basePath + "/" + _src;
290 | }
291 |
292 | // Memoize the loader function
293 | const imageLoader = useMemo(() => {
294 | return imageError || unoptimized === true
295 | ? () => fallbackLoader({ src: overrideSrc || src })
296 | : (e: { width: number }) =>
297 | optimizedLoader({ src, width: e.width, basePath });
298 | }, [imageError, unoptimized, overrideSrc, src, basePath]);
299 |
300 | const handleError = useCallback(
301 | (error: any) => {
302 | setImageError(true);
303 | setBlurComplete(true);
304 | // execute the onError function if provided
305 | onError && onError(error);
306 | },
307 | [onError]
308 | );
309 |
310 | const handleLoad = useCallback(
311 | (e: any) => {
312 | // for some configurations, the onError handler is not called on an error occurrence
313 | // so we need to check if the image is loaded correctly
314 | const target = e.target as HTMLImageElement;
315 | if (target.naturalWidth === 0) {
316 | // Broken image, fall back to unoptimized (meaning the original image src)
317 | setImageError(true);
318 | }
319 | setBlurComplete(true);
320 |
321 | // execute the onLoad callback if present
322 | onLoad && onLoad(e);
323 | },
324 | [onLoad]
325 | );
326 |
327 | return (
328 |
354 | );
355 | }
356 | );
357 | ExportedImage.displayName = "ExportedImage";
358 | export default ExportedImage;
359 |
--------------------------------------------------------------------------------
/example/src/legacy/ExportedImage.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useMemo, useState } from "react";
4 | import Image, { ImageProps, StaticImageData } from "next/legacy/image";
5 |
6 | const splitFilePath = ({ filePath }: { filePath: string }) => {
7 | const filenameWithExtension =
8 | filePath.split("\\").pop()?.split("/").pop() || "";
9 | const filePathWithoutFilename = filePath.split(filenameWithExtension).shift();
10 | const fileExtension = filePath.split(".").pop();
11 | const filenameWithoutExtension =
12 | filenameWithExtension.substring(
13 | 0,
14 | filenameWithExtension.lastIndexOf(".")
15 | ) || filenameWithExtension;
16 | return {
17 | path: filePathWithoutFilename,
18 | filename: filenameWithoutExtension,
19 | extension: fileExtension || "",
20 | };
21 | };
22 |
23 | const generateImageURL = (
24 | src: string,
25 | width: number,
26 | basePath: string | undefined,
27 | isRemoteImage: boolean = false
28 | ) => {
29 | const { filename, path, extension } = splitFilePath({ filePath: src });
30 | const useWebp =
31 | process.env.nextImageExportOptimizer_storePicturesInWEBP != undefined
32 | ? process.env.nextImageExportOptimizer_storePicturesInWEBP == "true"
33 | : true;
34 | if (
35 | !["JPG", "JPEG", "WEBP", "PNG", "AVIF", "GIF"].includes(
36 | extension.toUpperCase()
37 | )
38 | ) {
39 | // The images has an unsupported extension
40 | // We will return the src
41 | return src;
42 | }
43 | // If the images are stored as WEBP by the package, then we should change
44 | // the extension to WEBP to load them correctly
45 | let processedExtension = extension;
46 |
47 | if (
48 | useWebp &&
49 | ["JPG", "JPEG", "PNG", "GIF"].includes(extension.toUpperCase())
50 | ) {
51 | processedExtension = "WEBP";
52 | }
53 |
54 | let correctedPath = path;
55 | const lastChar = correctedPath?.substr(-1); // Selects the last character
56 | if (lastChar != "/") {
57 | // If the last character is not a slash
58 | correctedPath = correctedPath + "/"; // Append a slash to it.
59 | }
60 |
61 | const isStaticImage = src.includes("_next/static/media");
62 |
63 | if (!isStaticImage && basePath) {
64 | correctedPath = basePath + "/" + correctedPath;
65 | }
66 |
67 | const exportFolderName =
68 | process.env.nextImageExportOptimizer_exportFolderName ||
69 | "nextImageExportOptimizer";
70 | const basePathPrefixForStaticImages = basePath ? basePath + "/" : "";
71 |
72 | let generatedImageURL = `${
73 | isStaticImage ? basePathPrefixForStaticImages : correctedPath
74 | }${exportFolderName}/${filename}-opt-${width}.${processedExtension.toUpperCase()}`;
75 |
76 | // if the generatedImageURL is not starting with a slash, then we add one as long as it is not a remote image
77 | if (!isRemoteImage && generatedImageURL.charAt(0) !== "/") {
78 | generatedImageURL = "/" + generatedImageURL;
79 | }
80 |
81 | return generatedImageURL;
82 | };
83 |
84 | // Credits to https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
85 | // This is a hash function that is used to generate a hash from the image URL
86 | const hashAlgorithm = (str: string, seed = 0) => {
87 | let h1 = 0xdeadbeef ^ seed,
88 | h2 = 0x41c6ce57 ^ seed;
89 | for (let i = 0, ch; i < str.length; i++) {
90 | ch = str.charCodeAt(i);
91 | h1 = Math.imul(h1 ^ ch, 2654435761);
92 | h2 = Math.imul(h2 ^ ch, 1597334677);
93 | }
94 | h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
95 | h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
96 | h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
97 | h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
98 |
99 | return 4294967296 * (2097151 & h2) + (h1 >>> 0);
100 | };
101 |
102 | function urlToFilename(url: string) {
103 | try {
104 | const parsedUrl = new URL(url);
105 | const extension = parsedUrl.pathname.split(".").pop();
106 | if (extension) {
107 | return hashAlgorithm(url).toString().concat(".", extension);
108 | }
109 | } catch (error) {
110 | console.error("Error parsing URL", url, error);
111 | }
112 | return hashAlgorithm(url).toString();
113 | }
114 |
115 | const imageURLForRemoteImage = ({
116 | src,
117 | width,
118 | basePath,
119 | }: {
120 | src: string;
121 | width: number;
122 | basePath: string | undefined;
123 | }) => {
124 | const encodedSrc = urlToFilename(src);
125 |
126 | return generateImageURL(encodedSrc, width, basePath, true);
127 | };
128 |
129 | const optimizedLoader = ({
130 | src,
131 | width,
132 | basePath,
133 | }: {
134 | src: string | StaticImageData;
135 | width: number;
136 | basePath: string | undefined;
137 | }) => {
138 | const isStaticImage = typeof src === "object";
139 | const _src = isStaticImage ? src.src : src;
140 | const originalImageWidth = (isStaticImage && src.width) || undefined;
141 |
142 | // if it is a static image, we can use the width of the original image to generate a reduced srcset that returns
143 | // the same image url for widths that are larger than the original image
144 | if (isStaticImage && originalImageWidth && width > originalImageWidth) {
145 | const deviceSizes = process.env.__NEXT_IMAGE_OPTS?.deviceSizes || [
146 | 640, 750, 828, 1080, 1200, 1920, 2048, 3840,
147 | ];
148 | const imageSizes = process.env.__NEXT_IMAGE_OPTS?.imageSizes || [
149 | 16, 32, 48, 64, 96, 128, 256, 384,
150 | ];
151 | const allSizes = [...deviceSizes, ...imageSizes];
152 |
153 | // only use the width if it is smaller or equal to the next size in the allSizes array
154 | let nextLargestSize = null;
155 | for (let i = 0; i < allSizes.length; i++) {
156 | if (
157 | Number(allSizes[i]) >= originalImageWidth &&
158 | (nextLargestSize === null || Number(allSizes[i]) < nextLargestSize)
159 | ) {
160 | nextLargestSize = Number(allSizes[i]);
161 | }
162 | }
163 |
164 | if (nextLargestSize !== null) {
165 | return generateImageURL(_src, nextLargestSize, basePath);
166 | }
167 | }
168 |
169 | // Check if the image is a remote image (starts with http or https)
170 | if (_src.startsWith("http")) {
171 | return imageURLForRemoteImage({ src: _src, width, basePath });
172 | }
173 |
174 | return generateImageURL(_src, width, basePath);
175 | };
176 |
177 | const fallbackLoader = ({
178 | src,
179 | basePath,
180 | }: {
181 | src: string | StaticImageData;
182 | basePath: string | undefined;
183 | }) => {
184 | let _src = typeof src === "object" ? src.src : src;
185 | const isRemoteImage = _src.startsWith("http");
186 |
187 | // if the _src does not start with a slash, then we add one as long as it is not a remote image
188 | if (!isRemoteImage && _src.charAt(0) !== "/") {
189 | _src = "/" + _src;
190 | }
191 |
192 | if (basePath) {
193 | _src = basePath + _src;
194 | }
195 | return _src;
196 | };
197 |
198 | export interface ExportedImageProps
199 | extends Omit {
200 | src: string | StaticImageData;
201 | basePath?: string;
202 | }
203 |
204 | function ExportedImage({
205 | src,
206 | priority = false,
207 | loading,
208 | lazyRoot = null,
209 | lazyBoundary = "200px",
210 | className,
211 | width,
212 | height,
213 | objectFit,
214 | objectPosition,
215 | layout,
216 | onLoadingComplete,
217 | unoptimized,
218 | alt = "",
219 | placeholder = "blur",
220 | basePath = "",
221 | blurDataURL,
222 | onError,
223 | ...rest
224 | }: ExportedImageProps) {
225 | const [imageError, setImageError] = useState(false);
226 |
227 | const automaticallyCalculatedBlurDataURL = useMemo(() => {
228 | if (blurDataURL) {
229 | // use the user provided blurDataURL if present
230 | return blurDataURL;
231 | }
232 | // check if the src is specified as a local file -> then it is an object
233 | const isStaticImage = typeof src === "object";
234 | let _src = isStaticImage ? src.src : src;
235 | if (unoptimized === true) {
236 | // return the src image when unoptimized
237 | if (!isStaticImage) {
238 | if (basePath && _src.startsWith("/")) {
239 | _src = basePath + _src;
240 | }
241 | if (basePath && !_src.startsWith("/")) {
242 | _src = basePath + "/" + _src;
243 | }
244 | }
245 |
246 | return _src;
247 | }
248 | // Check if the image is a remote image (starts with http or https)
249 | if (_src.startsWith("http")) {
250 | return imageURLForRemoteImage({ src: _src, width: 10, basePath });
251 | }
252 |
253 | // otherwise use the generated image of 10px width as a blurDataURL
254 | return generateImageURL(_src, 10, basePath);
255 | }, [blurDataURL, src, unoptimized, basePath]);
256 | const isStaticImage = typeof src === "object";
257 | let _src = isStaticImage ? src.src : src;
258 | if (!isStaticImage) {
259 | if (basePath && _src.startsWith("/")) {
260 | _src = basePath + _src;
261 | }
262 | if (basePath && !_src.startsWith("/")) {
263 | _src = basePath + "/" + _src;
264 | }
265 | }
266 |
267 | return (
268 | fallbackLoader({ src, basePath })
294 | : (e) => optimizedLoader({ src, width: e.width, basePath })
295 | }
296 | blurDataURL={automaticallyCalculatedBlurDataURL}
297 | onError={(error) => {
298 | setImageError(true);
299 | // execute the onError function if provided
300 | onError && onError(error);
301 | }}
302 | onLoadingComplete={(result) => {
303 | // for some configurations, the onError handler is not called on an error occurrence
304 | // so we need to check if the image is loaded correctly
305 | if (result.naturalWidth === 0) {
306 | // Broken image, fall back to unoptimized (meaning the original image src)
307 | setImageError(true);
308 | }
309 | // execute the onLoadingComplete callback if present
310 | onLoadingComplete && onLoadingComplete(result);
311 | }}
312 | src={_src}
313 | />
314 | );
315 | }
316 |
317 | export default ExportedImage;
318 |
--------------------------------------------------------------------------------
/example/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 0 2rem;
3 | }
4 |
5 | .main {
6 | min-height: 100vh;
7 | padding: 4rem 0;
8 | flex: 1;
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | .footer {
16 | display: flex;
17 | flex: 1;
18 | padding: 2rem 0;
19 | border-top: 1px solid #eaeaea;
20 | justify-content: center;
21 | align-items: center;
22 | }
23 |
24 | .footer a {
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | flex-grow: 1;
29 | }
30 |
31 | .title a {
32 | color: #0070f3;
33 | text-decoration: none;
34 | }
35 |
36 | .title a:hover,
37 | .title a:focus,
38 | .title a:active {
39 | text-decoration: underline;
40 | }
41 |
42 | .title {
43 | margin: 0;
44 | line-height: 1.15;
45 | font-size: 4rem;
46 | padding-bottom: 2rem;
47 | }
48 |
49 | .title,
50 | .description {
51 | text-align: center;
52 | }
53 |
54 | .description {
55 | margin: 4rem 0;
56 | line-height: 1.5;
57 | font-size: 1.5rem;
58 | }
59 |
60 | .code {
61 | background: #fafafa;
62 | border-radius: 5px;
63 | padding: 0.75rem;
64 | font-size: 1.1rem;
65 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
66 | Bitstream Vera Sans Mono, Courier New, monospace;
67 | }
68 |
69 | .grid {
70 | display: flex;
71 | align-items: center;
72 | justify-content: center;
73 | flex-wrap: wrap;
74 | max-width: 800px;
75 | }
76 |
77 | .card {
78 | margin: 1rem;
79 | padding: 1.5rem;
80 | text-align: left;
81 | color: inherit;
82 | text-decoration: none;
83 | border: 1px solid #eaeaea;
84 | border-radius: 10px;
85 | transition: color 0.15s ease, border-color 0.15s ease;
86 | max-width: 300px;
87 | }
88 |
89 | .card:hover,
90 | .card:focus,
91 | .card:active {
92 | color: #0070f3;
93 | border-color: #0070f3;
94 | }
95 |
96 | .card h2 {
97 | margin: 0 0 1rem 0;
98 | font-size: 1.5rem;
99 | }
100 |
101 | .card p {
102 | margin: 0;
103 | font-size: 1.25rem;
104 | line-height: 1.5;
105 | }
106 |
107 | .logo {
108 | height: 1em;
109 | margin-left: 0.5rem;
110 | }
111 |
112 | @media (max-width: 600px) {
113 | .grid {
114 | width: 100%;
115 | flex-direction: column;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/example/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
--------------------------------------------------------------------------------
/example/test/e2e/fixedImageSizeTest.spec.mjs:
--------------------------------------------------------------------------------
1 | import { test, expect } from "@playwright/test";
2 | import getImageById from "./getImageById.js";
3 |
4 | // get the environment variable flag for the test
5 | const testBasePath = process.env.BASEPATH === "true";
6 | const basePath = testBasePath ? "/subsite" : "";
7 | const imagesWebP =
8 | process.env.IMAGESWEBP === "true" || process.env.IMAGESWEBP === undefined;
9 |
10 | const widths = [16, 32, 48, 64, 96, 128, 256, 384];
11 | const correctSrc = {
12 | 16: [
13 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-16.${
14 | imagesWebP ? "WEBP" : "JPG"
15 | }`,
16 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-32.${
17 | imagesWebP ? "WEBP" : "JPG"
18 | }`,
19 | ],
20 | 32: [
21 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-32.${
22 | imagesWebP ? "WEBP" : "JPG"
23 | }`,
24 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-64.${
25 | imagesWebP ? "WEBP" : "JPG"
26 | }`,
27 | ],
28 | 48: [
29 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-48.${
30 | imagesWebP ? "WEBP" : "JPG"
31 | }`,
32 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-96.${
33 | imagesWebP ? "WEBP" : "JPG"
34 | }`,
35 | ],
36 | 64: [
37 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-64.${
38 | imagesWebP ? "WEBP" : "JPG"
39 | }`,
40 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-128.${
41 | imagesWebP ? "WEBP" : "JPG"
42 | }`,
43 | ],
44 | 96: [
45 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-96.${
46 | imagesWebP ? "WEBP" : "JPG"
47 | }`,
48 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-256.${
49 | imagesWebP ? "WEBP" : "JPG"
50 | }`,
51 | ],
52 | 128: [
53 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-128.${
54 | imagesWebP ? "WEBP" : "JPG"
55 | }`,
56 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-256.${
57 | imagesWebP ? "WEBP" : "JPG"
58 | }`,
59 | ],
60 | 256: [
61 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-256.${
62 | imagesWebP ? "WEBP" : "JPG"
63 | }`,
64 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-512.${
65 | imagesWebP ? "WEBP" : "JPG"
66 | }`,
67 | ],
68 | 384: [
69 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-384.${
70 | imagesWebP ? "WEBP" : "JPG"
71 | }`,
72 | `http://localhost:8080${basePath}/images/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash-opt-768.${
73 | imagesWebP ? "WEBP" : "JPG"
74 | }`,
75 | ],
76 | };
77 |
78 | for (let index = 0; index < widths.length; index++) {
79 | const width = widths[index];
80 |
81 | test.describe(`Test fixed width: ${width}`, () => {
82 | test.use({
83 | viewport: { width: 1024, height: 1024 },
84 | deviceScaleFactor: 1,
85 | });
86 | test("should check the image size", async ({ page }) => {
87 | await page.goto(`${basePath}/fixedImage`, {
88 | waitUntil: "networkidle",
89 | });
90 |
91 | await page.click("text=Next-Image-Export-Optimizer");
92 |
93 | const img = await page.locator(`#test_image_${width}`);
94 | await img.click();
95 | const testWidth = width;
96 |
97 | const image = await getImageById(page, `test_image_${testWidth}`);
98 |
99 | expect(
100 | correctSrc[width.toString()].includes(image.currentSrc)
101 | ).toBeTruthy();
102 | const image_future = await getImageById(
103 | page,
104 | `test_image_${testWidth}_future`
105 | );
106 |
107 | expect(
108 | correctSrc[width.toString()].includes(image_future.currentSrc)
109 | ).toBeTruthy();
110 |
111 | // check the number of images on the page
112 | const images = await page.$$("img");
113 | expect(images.length).toBe(24);
114 | });
115 | });
116 | }
117 |
--------------------------------------------------------------------------------
/example/test/e2e/getImageById.js:
--------------------------------------------------------------------------------
1 | module.exports = async function getImageById(page, imageId) {
2 | return await page.evaluate((imageId) => {
3 | let img = document.getElementById(imageId);
4 | return {
5 | src: img.src,
6 | currentSrc: img.currentSrc,
7 | naturalWidth: img.naturalWidth,
8 | width: img.width,
9 | srcset: img.srcset,
10 | };
11 | }, imageId);
12 | };
13 |
--------------------------------------------------------------------------------
/example/test/e2e/unoptimizedTest.spec.mjs:
--------------------------------------------------------------------------------
1 | import { test, expect } from "@playwright/test";
2 | import getImageById from "./getImageById.js";
3 |
4 | // get the environment variable flag for the test
5 | const testBasePath = process.env.BASEPATH === "true";
6 | const basePath = testBasePath ? "/subsite" : "";
7 |
8 | test.describe(`Test unoptimized image prop`, () => {
9 | test.use({
10 | viewport: { width: 1200, height: 1200 * 3 },
11 | deviceScaleFactor: 1,
12 | });
13 | test("should check the image size", async ({ page }) => {
14 | await page.goto(`${basePath}/`, {
15 | waitUntil: "networkidle",
16 | });
17 |
18 | await page.click("text=Next-Image-Export-Optimizer");
19 |
20 | const img = await page.locator("#test_image_unoptimized");
21 | await img.click();
22 |
23 | const image = await getImageById(page, "test_image_unoptimized");
24 |
25 | expect(image.currentSrc).toBe(
26 | `http://localhost:8080${basePath}/images/chris-zhang-Jq8-3Bmh1pQ-unsplash.jpg`
27 | );
28 | const img_legacy = await page.locator("#test_image_unoptimized_legacy");
29 | await img_legacy.click();
30 |
31 | const image_legacy = await getImageById(
32 | page,
33 | "test_image_unoptimized_legacy"
34 | );
35 |
36 | expect(image_legacy.currentSrc).toBe(
37 | `http://localhost:8080${basePath}/images/chris-zhang-Jq8-3Bmh1pQ-unsplash.jpg`
38 | );
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/example/testServer.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const path = require("path");
3 | const app = express();
4 | const port = 8080;
5 |
6 | // get the environment variable flag for the test
7 | const testBasePath = process.env.BASEPATH === "true";
8 | const basePath = testBasePath ? "/subsite" : "";
9 |
10 | const outPath = path.join(__dirname, "out");
11 |
12 | app.use(
13 | basePath,
14 | express.static(outPath, {
15 | extensions: ["html", "htm"],
16 | })
17 | );
18 |
19 | app.listen(port, () => {
20 | if (testBasePath)
21 | console.log(`Server running at http://localhost:${port}/subsite`);
22 | else console.log(`Server running at http://localhost:${port}`);
23 | });
24 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-image-export-optimizer",
3 | "version": "1.19.0",
4 | "description": "Optimizes all static images for Next.js static HTML export functionality",
5 | "main": "dist/ExportedImage.js",
6 | "types": "dist/ExportedImage.d.ts",
7 | "bin": "dist/optimizeImages.js",
8 | "engines": {
9 | "node": ">=16.0.0"
10 | },
11 | "exports": {
12 | ".": {
13 | "types": "./dist/ExportedImage.d.ts",
14 | "require": "./dist/ExportedImage.js",
15 | "import": "./dist/ExportedImage.js"
16 | },
17 | "./legacy/ExportedImage": {
18 | "types": "./dist/legacy/ExportedImage.d.ts",
19 | "require": "./dist/legacy/ExportedImage.js",
20 | "import": "./dist/legacy/ExportedImage.js"
21 | }
22 | },
23 | "typesVersions": {
24 | "*": {
25 | "legacy/ExportedImage": [
26 | "./dist/legacy/ExportedImage.d.ts"
27 | ]
28 | }
29 | },
30 | "files": [
31 | "dist/ExportedImage.js",
32 | "dist/ExportedImage.d.ts",
33 | "dist/legacy/ExportedImage.js",
34 | "dist/legacy/ExportedImage.d.ts",
35 | "dist/optimizeImages.js",
36 | "dist/utils"
37 | ],
38 | "scripts": {
39 | "build": "rimraf dist && tsc && tsc --project tsconfig.optimizeImages.json",
40 | "test": "jest",
41 | "test:e2e:basePath": "npx playwright install && BASEPATH=true IMAGESWEBP=false playwright test --config playwright-basePath.config.js",
42 | "test:e2e": "npx playwright install && playwright test",
43 | "exportExample": "cd example && npm run export && cd ..",
44 | "prepublishOnly": "npm run build && npm test && npm run test:e2e:basePath && npm run test:e2e",
45 | "fetchTags": "git fetch --tags -f"
46 | },
47 | "repository": {
48 | "type": "git",
49 | "url": "https://github.com/Niels-IO/next-image-export-optimizer"
50 | },
51 | "keywords": [
52 | "next.js",
53 | "next",
54 | "static",
55 | "export",
56 | "image",
57 | "optimization",
58 | "webp",
59 | "sharp"
60 | ],
61 | "author": "Niels Grafen",
62 | "license": "MIT",
63 | "dependencies": {
64 | "sharp": "^0.33.1",
65 | "typescript": "^5.2.2"
66 | },
67 | "peerDependencies": {
68 | "next": "^14.2.18 || ^15.0.3",
69 | "react": "^18.2.0 || ^19.0.0-0"
70 | },
71 | "devDependencies": {
72 | "@next/eslint-plugin-next": "^15.0.3",
73 | "@playwright/test": "^1.39.0",
74 | "@types/react": "npm:types-react@rc",
75 | "eslint": "^8.57.0",
76 | "eslint-plugin-react": "^7.33.2",
77 | "jest": "^29.7.0",
78 | "rimraf": "^6.0.1",
79 | "ts-node": "^10.9.1"
80 | },
81 | "overrides": {
82 | "@types/react": "npm:types-react@rc",
83 | "@types/react-dom": "npm:types-react-dom@rc"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/playwright-basePath.config.js:
--------------------------------------------------------------------------------
1 | const { devices } = require("@playwright/test");
2 | const fs = require("fs");
3 |
4 | const newConfigBasePath = `
5 | import type { NextConfig } from 'next'
6 |
7 | const nextConfig: NextConfig = {
8 | images: {
9 | loader: "custom",
10 | imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
11 | deviceSizes: [640, 750, 777, 828, 1080, 1200, 1920, 2048, 3840],
12 | },
13 | basePath: "/subsite",
14 | output: "export",
15 | transpilePackages: ["next-image-export-optimizer"],
16 | env: {
17 | nextImageExportOptimizer_imageFolderPath: "public/images",
18 | nextImageExportOptimizer_exportFolderPath: "out",
19 | nextImageExportOptimizer_exportFolderName: "nextImageExportOptimizer",
20 | nextImageExportOptimizer_quality: "75",
21 | nextImageExportOptimizer_storePicturesInWEBP: "false",
22 | nextImageExportOptimizer_generateAndUseBlurImages: "true",
23 | },
24 | };
25 | export default nextConfig
26 | `;
27 | // write config file for the to be tested configuration variables to the folder
28 | fs.writeFileSync("example/next.config.ts", newConfigBasePath);
29 | const config = {
30 | use: {
31 | baseURL: "http://localhost:8080/",
32 | },
33 | testDir: "example/test/e2e",
34 | projects: [
35 | {
36 | name: "chromium",
37 | use: { ...devices["Desktop Chrome"] },
38 | },
39 | {
40 | name: "firefox",
41 | use: { ...devices["Desktop Firefox"] },
42 | },
43 | ],
44 | webServer: {
45 | command:
46 | "cd example && npm run export && BASEPATH=true IMAGESWEBP=false node testServer.js",
47 | port: 8080,
48 | timeout: 120 * 1000,
49 | reuseExistingServer: false,
50 | stdout: "pipe",
51 | },
52 | };
53 | module.exports = config;
54 |
--------------------------------------------------------------------------------
/playwright.config.js:
--------------------------------------------------------------------------------
1 | const { devices } = require("@playwright/test");
2 | const fs = require("fs");
3 |
4 | const newConfigBasePath = `
5 | import type { NextConfig } from 'next'
6 |
7 | const nextConfig: NextConfig = {
8 | images: {
9 | loader: "custom",
10 | imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
11 | deviceSizes: [640, 750, 777, 828, 1080, 1200, 1920, 2048, 3840],
12 | },
13 | output: "export",
14 | transpilePackages: ["next-image-export-optimizer"],
15 | env: {
16 | nextImageExportOptimizer_imageFolderPath: "public/images",
17 | nextImageExportOptimizer_exportFolderPath: "out",
18 | nextImageExportOptimizer_exportFolderName: "nextImageExportOptimizer",
19 | nextImageExportOptimizer_quality: "75",
20 | nextImageExportOptimizer_storePicturesInWEBP: "true",
21 | nextImageExportOptimizer_generateAndUseBlurImages: "true",
22 | },
23 | };
24 | export default nextConfig
25 | `;
26 | // write config file for the to be tested configuration variables to the folder
27 | fs.writeFileSync("example/next.config.ts", newConfigBasePath);
28 | const config = {
29 | use: {
30 | baseURL: "http://localhost:8080/",
31 | },
32 | testDir: "example/test/e2e",
33 | projects: [
34 | {
35 | name: "chromium",
36 | use: { ...devices["Desktop Chrome"] },
37 | },
38 | {
39 | name: "firefox",
40 | use: { ...devices["Desktop Firefox"] },
41 | },
42 | ],
43 | webServer: {
44 | command: "cd example && npm run export && cd out/ && npx serve -p 8080",
45 | port: 8080,
46 | timeout: 120 * 1000,
47 | reuseExistingServer: false,
48 | },
49 | };
50 | module.exports = config;
51 |
--------------------------------------------------------------------------------
/src/optimizeImages.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { ImageObject } from "./utils/ImageObject";
4 |
5 | const defineProgressBar = require("./utils/defineProgressBar");
6 | const ensureDirectoryExists = require("./utils/ensureDirectoryExists");
7 | const getAllFilesAsObject = require("./utils/getAllFilesAsObject");
8 | const getHash = require("./utils/getHash");
9 | import { getRemoteImageURLs } from "./utils/getRemoteImageURLs";
10 | import { downloadImagesInBatches } from "./utils/downloadImagesInBatches";
11 |
12 | const urlToFilename = require("./utils/urlToFilename");
13 |
14 | const fs = require("fs");
15 | const sharp = require("sharp");
16 | const path = require("path");
17 |
18 | const loadConfig = require("next/dist/server/config").default;
19 |
20 | // Check if the --name and --age arguments are present
21 | const nextConfigPathIndex = process.argv.indexOf("--nextConfigPath");
22 | const exportFolderPathIndex = process.argv.indexOf("--exportFolderPath");
23 |
24 | // Check if there is only one argument without a name present -> this is the case if the user does not provide the path to the next.config.[js/ts] file
25 | if (process.argv.length === 3) {
26 | // Colorize the output to red
27 | // Colorize the output to red
28 | console.error("\x1b[31m");
29 | console.error(
30 | "next-image-export-optimizer: Breaking change: Please provide the path to the next.config.[js/ts] file as an argument with the name --nextConfigPath."
31 | );
32 | // Reset the color
33 | console.error("\x1b[0m");
34 | process.exit(1);
35 | }
36 |
37 | // Set the nextConfigPath and exportFolderPath variables to the corresponding arguments, or to undefined if the arguments are not present
38 | let nextConfigPath =
39 | nextConfigPathIndex !== -1
40 | ? process.argv[nextConfigPathIndex + 1]
41 | : undefined;
42 | let exportFolderPathCommandLine =
43 | exportFolderPathIndex !== -1
44 | ? process.argv[exportFolderPathIndex + 1]
45 | : undefined;
46 |
47 | if (nextConfigPath) {
48 | nextConfigPath = path.isAbsolute(nextConfigPath)
49 | ? nextConfigPath
50 | : path.join(process.cwd(), nextConfigPath);
51 | } else {
52 | // Check for next.config.js, next.config.ts, and next.config.mjs
53 | const jsConfigPath = path.join(process.cwd(), "next.config.js");
54 | const tsConfigPath = path.join(process.cwd(), "next.config.ts");
55 | const mjsConfigPath = path.join(process.cwd(), "next.config.mjs");
56 |
57 | if (fs.existsSync(jsConfigPath)) {
58 | nextConfigPath = jsConfigPath;
59 | } else if (fs.existsSync(tsConfigPath)) {
60 | nextConfigPath = tsConfigPath;
61 | } else if (fs.existsSync(mjsConfigPath)) {
62 | nextConfigPath = mjsConfigPath;
63 | } else {
64 | console.error("\x1b[31m");
65 | console.error(
66 | "next-image-export-optimizer: Could not find next.config.js, next.config.ts, or next.config.mjs. Please provide the path to the configuration file."
67 | );
68 | console.error("\x1b[0m");
69 | process.exit(1);
70 | }
71 | }
72 | const nextConfigFolder = path.dirname(nextConfigPath);
73 |
74 | const folderNameForRemoteImages = `remoteImagesForOptimization`;
75 | const folderPathForRemoteImages = path.join(
76 | nextConfigFolder,
77 | folderNameForRemoteImages
78 | );
79 |
80 | if (exportFolderPathCommandLine) {
81 | exportFolderPathCommandLine = path.isAbsolute(exportFolderPathCommandLine)
82 | ? exportFolderPathCommandLine
83 | : path.join(process.cwd(), exportFolderPathCommandLine);
84 | }
85 |
86 | const nextImageExportOptimizer = async function () {
87 | console.log(
88 | "---- next-image-export-optimizer: Begin with optimization... ---- "
89 | );
90 |
91 | // Default values
92 | let imageFolderPath = "public/images";
93 | let staticImageFolderPath = ".next/static/media";
94 | let exportFolderPath = "out";
95 | let deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
96 | let imageSizes = [16, 32, 48, 64, 96, 128, 256, 384];
97 | let quality = 75;
98 | let storePicturesInWEBP = true;
99 | let blurSize: number[] = [];
100 | let remoteImageCacheTTL = 0;
101 | let exportFolderName = "nextImageExportOptimizer";
102 | let remoteImageFileName = "remoteOptimizedImages.js";
103 |
104 | let remoteImageFilenames: {
105 | basePath: string;
106 | file: any;
107 | dirPathWithoutBasePath: string;
108 | fullPath: string;
109 | }[] = [];
110 | let remoteImageURLs: string[] = [];
111 |
112 | try {
113 | // Read in the configuration parameters
114 | const nextjsConfig = await loadConfig("phase-export", nextConfigFolder);
115 |
116 | // Check if nextjsConfig is an object or is undefined
117 | if (typeof nextjsConfig !== "object" || nextjsConfig === null) {
118 | throw new Error("next.config.[js/ts] is not an object");
119 | }
120 | const legacyPath = nextjsConfig.images?.nextImageExportOptimizer;
121 | const newPath = nextjsConfig.env;
122 |
123 | if (legacyPath?.remoteImagesFilename !== undefined) {
124 | remoteImageFileName = legacyPath.remoteImagesFilename;
125 | } else if (
126 | newPath?.nextImageExportOptimizer_remoteImagesFilename !== undefined
127 | ) {
128 | remoteImageFileName =
129 | newPath.nextImageExportOptimizer_remoteImagesFilename;
130 | }
131 |
132 | if (legacyPath?.imageFolderPath !== undefined) {
133 | imageFolderPath = legacyPath.imageFolderPath;
134 | } else if (
135 | newPath?.nextImageExportOptimizer_imageFolderPath !== undefined
136 | ) {
137 | imageFolderPath = newPath.nextImageExportOptimizer_imageFolderPath;
138 | // if the imageFolderPath starts with a slash, remove it
139 | if (imageFolderPath.startsWith("/")) {
140 | imageFolderPath = imageFolderPath.slice(1);
141 | }
142 | }
143 | if (legacyPath?.exportFolderPath !== undefined) {
144 | exportFolderPath = legacyPath.exportFolderPath;
145 | } else if (
146 | newPath?.nextImageExportOptimizer_exportFolderPath !== undefined
147 | ) {
148 | exportFolderPath = newPath.nextImageExportOptimizer_exportFolderPath;
149 | }
150 | if (nextjsConfig.images?.deviceSizes !== undefined) {
151 | deviceSizes = nextjsConfig.images.deviceSizes;
152 | }
153 | if (nextjsConfig.images?.imageSizes !== undefined) {
154 | imageSizes = nextjsConfig.images.imageSizes;
155 | }
156 |
157 | if (legacyPath?.quality !== undefined) {
158 | quality = Number(legacyPath.quality);
159 | } else if (newPath?.nextImageExportOptimizer_quality !== undefined) {
160 | quality = Number(newPath.nextImageExportOptimizer_quality);
161 | }
162 | if (nextjsConfig.env?.storePicturesInWEBP !== undefined) {
163 | storePicturesInWEBP =
164 | nextjsConfig.env.storePicturesInWEBP.toLowerCase() == "true";
165 | } else if (
166 | newPath?.nextImageExportOptimizer_storePicturesInWEBP !== undefined
167 | ) {
168 | storePicturesInWEBP =
169 | newPath.nextImageExportOptimizer_storePicturesInWEBP.toLowerCase() ==
170 | "true";
171 | }
172 | if (nextjsConfig.env?.generateAndUseBlurImages?.toLowerCase() == "true") {
173 | blurSize = [10];
174 | } else if (
175 | newPath?.nextImageExportOptimizer_generateAndUseBlurImages == "true"
176 | ) {
177 | blurSize = [10];
178 | }
179 | if (newPath.nextImageExportOptimizer_exportFolderName !== undefined) {
180 | exportFolderName = newPath.nextImageExportOptimizer_exportFolderName;
181 | }
182 | if (newPath.nextImageExportOptimizer_remoteImageCacheTTL !== undefined) {
183 | remoteImageCacheTTL = Number(
184 | newPath.nextImageExportOptimizer_remoteImageCacheTTL
185 | );
186 | }
187 |
188 | // Give the user a warning if the transpilePackages: ["next-image-export-optimizer"], is not set in the next.config.[js/ts]
189 | if (
190 | nextjsConfig.transpilePackages === undefined || // transpilePackages is not set
191 | (nextjsConfig.transpilePackages !== undefined &&
192 | !nextjsConfig.transpilePackages.includes("next-image-export-optimizer")) // transpilePackages is set but does not include next-image-export-optimizer
193 | ) {
194 | console.warn(
195 | "\x1b[41m",
196 | `Changed in 1.2.0: You have not set transpilePackages: ["next-image-export-optimizer"] in your next.config.[js/ts]. This may cause problems with next-image-export-optimizer. Please add this line to your next.config.[js/ts].`,
197 | "\x1b[0m"
198 | );
199 | }
200 | } catch (e) {
201 | // Configuration file not found
202 | console.log(
203 | "Could not find a next.config.js or next.config.ts file. Use of default values"
204 | );
205 | } finally {
206 | const result = await getRemoteImageURLs(
207 | remoteImageFileName,
208 | nextConfigFolder,
209 | folderPathForRemoteImages
210 | );
211 | remoteImageFilenames = result.remoteImageFilenames;
212 | remoteImageURLs = result.remoteImageURLs;
213 | }
214 |
215 | // if the user has specified a path for the export folder via the command line, use this path
216 | exportFolderPath = exportFolderPathCommandLine || exportFolderPath;
217 |
218 | // Give the user a warning, if the public directory of Next.js is not found as the user
219 | // may have run the command in a wrong directory
220 | if (!fs.existsSync(path.join(nextConfigFolder, "public"))) {
221 | console.warn(
222 | "\x1b[41m",
223 | `Could not find a public folder in this directory. Make sure you run the command in the main directory of your project.`,
224 | "\x1b[0m"
225 | );
226 | }
227 |
228 | // Create the folder for the remote images if it does not exists
229 | if (remoteImageURLs.length > 0) {
230 | try {
231 | if (!fs.existsSync(folderNameForRemoteImages)) {
232 | fs.mkdirSync(folderNameForRemoteImages);
233 | console.log(
234 | `Create remote image output folder: ${folderNameForRemoteImages}`
235 | );
236 | }
237 | } catch (err) {
238 | console.error(err);
239 | }
240 | }
241 |
242 | // Download the remote images specified in the remoteOptimizedImages.js file
243 | if (remoteImageURLs.length > 0)
244 | console.log(
245 | `Found ${remoteImageURLs.length} remote image${
246 | remoteImageURLs.length > 1 ? "s" : ""
247 | }...`
248 | );
249 |
250 | // we clear all images in the remote image folder that are not in the remoteImageURLs array
251 | const allFilesInRemoteImageFolder: string[] = fs.existsSync(
252 | folderNameForRemoteImages
253 | )
254 | ? fs.readdirSync(folderNameForRemoteImages)
255 | : [];
256 | const encodedRemoteImageURLs = remoteImageURLs.map((url: string) =>
257 | urlToFilename(url)
258 | );
259 |
260 | function removeLastUpdated(str: string) {
261 | const suffix = ".lastUpdated";
262 | if (str.endsWith(suffix)) {
263 | return str.slice(0, -suffix.length);
264 | }
265 | return str;
266 | }
267 |
268 | for (const filename of allFilesInRemoteImageFolder) {
269 | if (
270 | encodedRemoteImageURLs.includes(filename) ||
271 | encodedRemoteImageURLs.includes(removeLastUpdated(filename))
272 | ) {
273 | // the filename is in the remoteImageURLs array or the filename without the .lastUpdated suffix
274 | // so we do not delete it
275 | continue;
276 | }
277 |
278 | fs.unlinkSync(path.join(folderNameForRemoteImages, filename));
279 |
280 | console.log(
281 | `Deleted ${filename} from remote image folder as it is not retrieved from ${remoteImageFileName}.`
282 | );
283 | }
284 |
285 | await downloadImagesInBatches(
286 | remoteImageURLs,
287 | remoteImageFilenames,
288 | folderPathForRemoteImages,
289 | Math.min(remoteImageURLs.length, 20),
290 | remoteImageCacheTTL
291 | );
292 |
293 | // Create or read the JSON containing the hashes of the images in the image directory
294 | let imageHashes: {
295 | [key: string]: string;
296 | } = {};
297 | const hashFilePath = `${imageFolderPath}/next-image-export-optimizer-hashes.json`;
298 | try {
299 | let rawData = fs.readFileSync(hashFilePath);
300 | imageHashes = JSON.parse(rawData);
301 | } catch (e) {
302 | // No image hashes yet
303 | }
304 |
305 | // check if the image folder is a subdirectory of the public folder
306 | // if not, the images in the image folder can only be static images and are taken from the static image folder (staticImageFolderPath)
307 | // so we do not add them to the images that need to be optimized
308 |
309 | const isImageFolderSubdirectoryOfPublicFolder =
310 | imageFolderPath.includes("public");
311 |
312 | // Generate a warning if the image folder is not a subdirectory of the public folder
313 | if (!isImageFolderSubdirectoryOfPublicFolder) {
314 | console.warn(
315 | "\x1b[41mWarning: The image folder is not a subdirectory of the public folder. The images in the image folder are not optimized.\x1b[0m"
316 | );
317 | }
318 |
319 | const allFilesInImageFolderAndSubdirectories =
320 | isImageFolderSubdirectoryOfPublicFolder
321 | ? getAllFilesAsObject(imageFolderPath, imageFolderPath, exportFolderName)
322 | : [];
323 | const allFilesInStaticImageFolder = getAllFilesAsObject(
324 | staticImageFolderPath,
325 | staticImageFolderPath,
326 | exportFolderName
327 | );
328 | // append the static image folder to the image array
329 | allFilesInImageFolderAndSubdirectories.push(...allFilesInStaticImageFolder);
330 |
331 | // append the remote images to the image array
332 | if (remoteImageURLs.length > 0) {
333 | // get all files in the remote image folder again, as we added extensions to the filenames
334 | // if they were not present in the URLs in remoteOptimizedImages.js
335 |
336 | const allFilesInRemoteImageFolder = fs.readdirSync(
337 | folderNameForRemoteImages
338 | );
339 |
340 | const remoteImageFiles = allFilesInRemoteImageFolder.map(
341 | (filename: string) => {
342 | const filenameFull = path.join(folderPathForRemoteImages, filename);
343 |
344 | return {
345 | basePath: folderPathForRemoteImages,
346 | file: filename,
347 | dirPathWithoutBasePath: "",
348 | fullPath: filenameFull,
349 | };
350 | }
351 | );
352 |
353 | // append the remote images to the image array
354 | allFilesInImageFolderAndSubdirectories.push(...remoteImageFiles);
355 | }
356 |
357 | const allImagesInImageFolder = allFilesInImageFolderAndSubdirectories.filter(
358 | (fileObject: ImageObject) => {
359 | if (fileObject === undefined) return false;
360 | if (fileObject.file === undefined) return false;
361 | // check if the file has a supported extension
362 | const filenameSplit = fileObject.file.split(".");
363 | if (filenameSplit.length === 1) return false;
364 | const extension = filenameSplit.pop()!.toUpperCase();
365 | // Only include file with image extensions
366 | return ["JPG", "JPEG", "WEBP", "PNG", "AVIF", "GIF"].includes(extension);
367 | }
368 | );
369 | console.log(
370 | `Found ${
371 | allImagesInImageFolder.length - remoteImageURLs.length
372 | } supported images in ${imageFolderPath}, static folder and subdirectories and ${
373 | remoteImageURLs.length
374 | } remote image${remoteImageURLs.length > 1 ? "s" : ""}.`
375 | );
376 |
377 | let widths = [...blurSize, ...imageSizes, ...deviceSizes];
378 |
379 | // sort the widths in ascending order to make sure the logic works for limiting the number of images
380 | widths.sort((a, b) => a - b);
381 |
382 | // remove duplicate widths from the array
383 | widths = widths.filter((item, index) => widths.indexOf(item) === index);
384 |
385 | const progressBar = defineProgressBar();
386 | if (allImagesInImageFolder.length > 0) {
387 | console.log(`Using sizes: ${widths.toString()}`);
388 | console.log(
389 | `Start optimization of ${allImagesInImageFolder.length} images with ${
390 | widths.length
391 | } sizes resulting in ${
392 | allImagesInImageFolder.length * widths.length
393 | } optimized images...`
394 | );
395 | progressBar.start(allImagesInImageFolder.length * widths.length, 0, {
396 | sizeOfGeneratedImages: 0,
397 | });
398 | }
399 | let sizeOfGeneratedImages = 0;
400 | const allGeneratedImages: string[] = [];
401 |
402 | const updatedImageHashes: {
403 | [key: string]: string;
404 | } = {};
405 |
406 | // Loop through all images
407 | for (let index = 0; index < allImagesInImageFolder.length; index++) {
408 | // try catch to catch errors in the loop and let the user know which image caused the error
409 | try {
410 | const file = allImagesInImageFolder[index].file;
411 | let fileDirectory = allImagesInImageFolder[index].dirPathWithoutBasePath;
412 | let basePath = allImagesInImageFolder[index].basePath;
413 |
414 | let extension = file.split(".").pop()!.toUpperCase();
415 | const imageBuffer = fs.readFileSync(
416 | path.join(basePath, fileDirectory, file)
417 | );
418 | const imageHash = getHash([
419 | imageBuffer,
420 | ...widths,
421 | quality,
422 | fileDirectory,
423 | file,
424 | ]);
425 | const keyForImageHashes = `${fileDirectory}/${file}`;
426 |
427 | let hashContentChanged = false;
428 | if (imageHashes[keyForImageHashes] !== imageHash) {
429 | hashContentChanged = true;
430 | }
431 | // Store image hash in temporary object
432 | updatedImageHashes[keyForImageHashes] = imageHash;
433 |
434 | let optimizedOriginalWidthImagePath;
435 | let optimizedOriginalWidthImageSizeInMegabytes;
436 |
437 | // Loop through all widths
438 | for (let indexWidth = 0; indexWidth < widths.length; indexWidth++) {
439 | const width = widths[indexWidth];
440 |
441 | const filename = path.parse(file).name;
442 | if (storePicturesInWEBP) {
443 | extension = "WEBP";
444 | }
445 |
446 | const isStaticImage = basePath === staticImageFolderPath;
447 | // for a static image, we copy the image to public/nextImageExportOptimizer or public/${exportFolderName}
448 | // and not the staticImageFolderPath
449 | // as the static image folder is deleted before each build
450 | const basePathToStoreOptimizedImages =
451 | isStaticImage ||
452 | basePath === path.join(nextConfigFolder, folderNameForRemoteImages)
453 | ? "public"
454 | : basePath;
455 | const optimizedFileNameAndPath = path.join(
456 | basePathToStoreOptimizedImages,
457 | fileDirectory,
458 | exportFolderName,
459 | `${filename}-opt-${width}.${extension.toUpperCase()}`
460 | );
461 |
462 | // Check if file is already in hash and specific size and quality is present in the
463 | // opt file directory
464 | if (
465 | !hashContentChanged &&
466 | keyForImageHashes in imageHashes &&
467 | fs.existsSync(optimizedFileNameAndPath)
468 | ) {
469 | const stats = fs.statSync(optimizedFileNameAndPath);
470 | const fileSizeInBytes = stats.size;
471 | const fileSizeInMegabytes = fileSizeInBytes / (1024 * 1024);
472 | sizeOfGeneratedImages += fileSizeInMegabytes;
473 | progressBar.increment({
474 | sizeOfGeneratedImages: sizeOfGeneratedImages.toFixed(1),
475 | });
476 | allGeneratedImages.push(optimizedFileNameAndPath);
477 |
478 | continue;
479 | }
480 |
481 | const transformer = sharp(imageBuffer, {
482 | animated: true,
483 | limitInputPixels: false, // disable pixel limit
484 | });
485 |
486 | transformer.rotate();
487 |
488 | const { width: metaWidth } = await transformer.metadata();
489 |
490 | // For a static image, we can skip the image optimization and the copying
491 | // of the image for images with a width greater than the original image width
492 | // we will stop the loop at the first image with a width greater than the original image width
493 | let nextLargestSize = -1;
494 | for (let i = 0; i < widths.length; i++) {
495 | if (
496 | Number(widths[i]) >= metaWidth &&
497 | (nextLargestSize === -1 || Number(widths[i]) < nextLargestSize)
498 | ) {
499 | nextLargestSize = Number(widths[i]);
500 | }
501 | }
502 |
503 | if (
504 | isStaticImage &&
505 | nextLargestSize !== -1 &&
506 | width > nextLargestSize
507 | ) {
508 | progressBar.increment({
509 | sizeOfGeneratedImages: sizeOfGeneratedImages.toFixed(1),
510 | });
511 | continue;
512 | }
513 |
514 | // If the original image's width is X, the optimized images are
515 | // identical for all widths >= X. Once we have generated the first of
516 | // these identical images, we can simply copy that file instead of redoing
517 | // the optimization.
518 | if (
519 | optimizedOriginalWidthImagePath &&
520 | optimizedOriginalWidthImageSizeInMegabytes
521 | ) {
522 | fs.copyFileSync(
523 | optimizedOriginalWidthImagePath,
524 | optimizedFileNameAndPath
525 | );
526 |
527 | sizeOfGeneratedImages += optimizedOriginalWidthImageSizeInMegabytes;
528 | progressBar.increment({
529 | sizeOfGeneratedImages: sizeOfGeneratedImages.toFixed(1),
530 | });
531 | allGeneratedImages.push(optimizedFileNameAndPath);
532 |
533 | continue;
534 | }
535 |
536 | const resize = metaWidth && metaWidth > width;
537 | if (resize) {
538 | transformer.resize(width);
539 | }
540 |
541 | if (extension === "AVIF") {
542 | if (transformer.avif) {
543 | const avifQuality = quality - 15;
544 | transformer.avif({
545 | quality: Math.max(avifQuality, 0),
546 | chromaSubsampling: "4:2:0", // same as webp
547 | });
548 | } else {
549 | transformer.webp({ quality });
550 | }
551 | } else if (extension === "WEBP" || storePicturesInWEBP) {
552 | transformer.webp({ quality });
553 | } else if (extension === "PNG") {
554 | transformer.png({ quality });
555 | } else if (extension === "JPEG" || extension === "JPG") {
556 | transformer.jpeg({ quality });
557 | } else if (extension === "GIF") {
558 | transformer.gif({ quality });
559 | }
560 |
561 | // Write the optimized image to the file system
562 | ensureDirectoryExists(optimizedFileNameAndPath);
563 | const info = await transformer.toFile(optimizedFileNameAndPath);
564 | const fileSizeInBytes = info.size;
565 | const fileSizeInMegabytes = fileSizeInBytes / (1024 * 1024);
566 | sizeOfGeneratedImages += fileSizeInMegabytes;
567 | progressBar.increment({
568 | sizeOfGeneratedImages: sizeOfGeneratedImages.toFixed(1),
569 | });
570 | allGeneratedImages.push(optimizedFileNameAndPath);
571 |
572 | if (!resize) {
573 | optimizedOriginalWidthImagePath = optimizedFileNameAndPath;
574 | optimizedOriginalWidthImageSizeInMegabytes = fileSizeInMegabytes;
575 | }
576 | }
577 | } catch (error) {
578 | console.log(
579 | `
580 | Error while optimizing image ${allImagesInImageFolder[index].file}
581 | ${error}
582 | `
583 | );
584 | // throw the error so that the process stops
585 | throw error;
586 | }
587 | }
588 | let data = JSON.stringify(updatedImageHashes, null, 4);
589 | ensureDirectoryExists(hashFilePath);
590 | fs.writeFileSync(hashFilePath, data);
591 |
592 | // Copy the optimized images to the build folder
593 |
594 | console.log("\nCopy optimized images to build folder...");
595 | for (let index = 0; index < allGeneratedImages.length; index++) {
596 | const filePath = allGeneratedImages[index];
597 | const fileInBuildFolder = path.join(
598 | exportFolderPath,
599 | (() => {
600 | const parts = filePath.split("public");
601 | if (parts.length > 1) {
602 | return parts.slice(1).join("public");
603 | } else {
604 | // Handle case where 'public' is not found
605 | return filePath;
606 | }
607 | })()
608 | );
609 |
610 | // Create the folder for the optimized images in the build directory if it does not exists
611 | ensureDirectoryExists(fileInBuildFolder);
612 | fs.copyFileSync(filePath, fileInBuildFolder);
613 | }
614 |
615 | function findSubfolders(
616 | rootPath: string,
617 | folderName: string,
618 | results: string[] = []
619 | ) {
620 | const items = fs.readdirSync(rootPath);
621 | for (const item of items) {
622 | const itemPath = path.join(rootPath, item);
623 | const stat = fs.statSync(itemPath);
624 | if (stat.isDirectory()) {
625 | if (item === folderName) {
626 | results.push(itemPath);
627 | }
628 | findSubfolders(itemPath, folderName, results);
629 | }
630 | }
631 | return results;
632 | }
633 |
634 | const optimizedImagesFolders = findSubfolders(
635 | imageFolderPath,
636 | exportFolderName
637 | );
638 | optimizedImagesFolders.push(`public/${exportFolderName}`);
639 |
640 | function findImageFiles(
641 | folderPath: string,
642 | extensions: string[],
643 | results: string[] = []
644 | ) {
645 | // check if the folder exists
646 | if (!fs.existsSync(folderPath)) {
647 | return results;
648 | }
649 | const items = fs.readdirSync(folderPath);
650 | for (const item of items) {
651 | const itemPath = path.join(folderPath, item);
652 | const stat = fs.statSync(itemPath);
653 | if (stat.isDirectory()) {
654 | findImageFiles(itemPath, extensions, results);
655 | } else {
656 | const ext = path.extname(item).toUpperCase();
657 | if (extensions.includes(ext)) {
658 | results.push(itemPath);
659 | }
660 | }
661 | }
662 | return results;
663 | }
664 |
665 | const imageExtensions = [".PNG", ".GIF", ".JPG", ".JPEG", ".AVIF", ".WEBP"];
666 |
667 | const imagePaths: string[] = [];
668 | for (const subfolderPath of optimizedImagesFolders) {
669 | const paths = findImageFiles(subfolderPath, imageExtensions);
670 | imagePaths.push(...paths);
671 | }
672 |
673 | // find the optimized images that are no longer used in the project
674 | const unusedImages: string[] = [];
675 | for (const imagePath of imagePaths) {
676 | const isUsed = allGeneratedImages.includes(imagePath);
677 | if (!isUsed) {
678 | unusedImages.push(imagePath);
679 | }
680 | }
681 | // delete the unused images
682 | for (const imagePath of unusedImages) {
683 | if (fs.existsSync(imagePath)) {
684 | fs.unlinkSync(imagePath);
685 | }
686 | }
687 | if (unusedImages.length > 0)
688 | console.log(
689 | `Deleted ${unusedImages.length} unused image${
690 | unusedImages.length > 1 ? "s" : ""
691 | } from the optimized images folders.`
692 | );
693 | progressBar.stop();
694 |
695 | console.log("---- next-image-export-optimizer: Done ---- ");
696 | process.exit(0);
697 | };
698 |
699 | if (require.main === module) {
700 | nextImageExportOptimizer();
701 | }
702 | module.exports = nextImageExportOptimizer;
703 |
--------------------------------------------------------------------------------
/src/utils/ImageObject.ts:
--------------------------------------------------------------------------------
1 | export type ImageObject = {
2 | basePath: string;
3 | dirPathWithoutBasePath: string;
4 | file: string;
5 | fullPath?: string;
6 | };
7 |
--------------------------------------------------------------------------------
/src/utils/defineProgressBar.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | module.exports = function defineProgressBar() {
4 | let startTime: number;
5 | let total: number;
6 | let current: number = 0;
7 | let sizeOfGeneratedImages: number = 0;
8 |
9 | const updateProgress = () => {
10 | const percentage = Math.floor((current / total) * 100);
11 | const barLength = 50;
12 | const filledLength = Math.floor((percentage * barLength) / 100);
13 | const bar = "=".repeat(filledLength) + "-".repeat(barLength - filledLength);
14 | const eta = Math.round(
15 | (((Date.now() - startTime) / current) * (total - current)) / 1000
16 | );
17 |
18 | // Use process.stdout.write for more consistent cross-platform behavior
19 | process.stdout.write(
20 | `\r${bar} ${percentage}% | ETA: ${eta}s | ${current}/${total} | Total size: ${sizeOfGeneratedImages.toFixed(
21 | 1
22 | )} MB`
23 | );
24 | };
25 |
26 | return {
27 | start: (totalValue: number, startValue: number, payload: any) => {
28 | total = totalValue;
29 | current = startValue;
30 | startTime = Date.now();
31 | sizeOfGeneratedImages = payload.sizeOfGeneratedImages;
32 | updateProgress();
33 | },
34 | increment: (payload: any) => {
35 | current++;
36 | sizeOfGeneratedImages = parseFloat(payload.sizeOfGeneratedImages);
37 | updateProgress();
38 | },
39 | stop: () => {
40 | const endTime = Date.now();
41 | const elapsedTime = endTime - startTime;
42 | process.stdout.write(
43 | `\nFinished optimization in: ${msToTime(elapsedTime)}\n`
44 | );
45 | },
46 | };
47 | };
48 |
49 | function msToTime(ms: number): string {
50 | let seconds = (ms / 1000).toFixed(1);
51 | let minutes = (ms / (1000 * 60)).toFixed(1);
52 | let hours = (ms / (1000 * 60 * 60)).toFixed(1);
53 | let days = (ms / (1000 * 60 * 60 * 24)).toFixed(1);
54 | if (parseFloat(seconds) < 60) return seconds + " seconds";
55 | else if (parseFloat(minutes) < 60) return minutes + " minutes";
56 | else if (parseFloat(hours) < 24) return hours + " hours";
57 | else return days + " days";
58 | }
59 |
--------------------------------------------------------------------------------
/src/utils/downloadImagesInBatches.ts:
--------------------------------------------------------------------------------
1 | import { ImageObject } from "./ImageObject";
2 | import path from "path";
3 | import fs from "fs";
4 | const http = require("http");
5 | const https = require("https");
6 | const urlModule = require("url"); // Import url module to parse the url
7 |
8 | async function downloadImage(url: string, filename: string, folder: string) {
9 | return new Promise((resolve, reject) => {
10 | // Choose the right http library:
11 | const httpLib = urlModule.parse(url).protocol === "http:" ? http : https;
12 |
13 | const request = httpLib.get(url, function (response: any) {
14 | if (response.statusCode !== 200) {
15 | console.error(
16 | `Error: Unable to download ${url} (status code: ${response.statusCode}).`
17 | );
18 | reject(new Error(`Status code: ${response.statusCode}`));
19 | return;
20 | }
21 | // check if the file is a valid image by checking the content type
22 | if (
23 | !response.headers["content-type"].startsWith("image/") &&
24 | !response.headers["content-type"].startsWith("application/octet-stream")
25 | ) {
26 | console.error(
27 | `Error: Unable to download ${url} (invalid content type: ${response.headers["content-type"]}).`
28 | );
29 | reject(
30 | new Error(`Invalid content type: ${response.headers["content-type"]}`)
31 | );
32 | return;
33 | }
34 |
35 | // Extract image format from response headers
36 | const contentType = response.headers["content-type"];
37 | let imageFormat = contentType.split("/").pop();
38 |
39 | // Further split on semicolon (;) if exists
40 | if (imageFormat.includes(";")) {
41 | imageFormat = imageFormat.split(";")[0];
42 | }
43 |
44 | // Further split on plus (+) if exists, e.g. image/svg+xml
45 | if (imageFormat.includes("+")) {
46 | imageFormat = imageFormat.split("+")[0];
47 | }
48 |
49 | // Check for jpeg and change it to jpg if necessary
50 | if (imageFormat === "jpeg") {
51 | imageFormat = "jpg";
52 | }
53 |
54 | // Check if filename already has an extension that matches the image format
55 | const regex = new RegExp(`.${imageFormat}$`, "i");
56 | const hasMatchingExtension = regex.test(filename);
57 |
58 | // Add appropriate extension to filename based on image format
59 | const formattedFilename = hasMatchingExtension
60 | ? filename
61 | : `${filename}.${imageFormat}`;
62 |
63 | fs.access(folder, fs.constants.W_OK, function (err: any) {
64 | if (err) {
65 | console.error(
66 | `Error: Unable to write to ${folder} (${err.message}).`
67 | );
68 | reject(err);
69 | return;
70 | }
71 | // on close, check the file size and reject if it's 0 otherwise resolve
72 | response
73 | .pipe(fs.createWriteStream(formattedFilename))
74 | .on("error", function (err: any) {
75 | console.error(
76 | `Error: Unable to save ${formattedFilename} (${err.message}).`
77 | );
78 | reject(err);
79 | })
80 | .on("close", function () {
81 | fs.stat(formattedFilename, function (err: any, stats: any) {
82 | if (err) {
83 | console.error(
84 | `Error: Unable to get the size of ${formattedFilename} (${err.message}).`
85 | );
86 | reject(err);
87 | return;
88 | }
89 |
90 | if (stats.size === 0) {
91 | console.error(
92 | `Error: Unable to save ${formattedFilename} (empty file).`
93 | );
94 | reject(new Error("Empty file"));
95 | return;
96 | }
97 | // to cache the image locally, we store a file with the same name as the image, but with a .lastUpdated extension and the timestamp
98 | storeLastUpdated({
99 | basePath: folder,
100 | file: formattedFilename,
101 | dirPathWithoutBasePath: "",
102 | fullPath: formattedFilename,
103 | });
104 |
105 | resolve();
106 | });
107 | });
108 | });
109 | });
110 | request.on("error", (err: Error) => {
111 | console.error(`Error: Unable to download ${url}.`, err);
112 | reject(err);
113 | });
114 | });
115 | }
116 |
117 | export async function downloadImagesInBatches(
118 | imagesURLs: string[],
119 | imageFileNames: ImageObject[],
120 | folder: string,
121 | batchSize: number,
122 | remoteImageCacheTTL: number
123 | ) {
124 | let downloadedImages = 0;
125 | let cachedImages = 0;
126 | const batches = Math.ceil(imagesURLs.length / batchSize); // determine the number of batches
127 | for (let i = 0; i < batches; i++) {
128 | const start = i * batchSize; // calculate the start index of the batch
129 | const end = Math.min(imagesURLs.length, start + batchSize); // calculate the end index of the batch
130 | const batchURLs = imagesURLs.slice(start, end); // slice the URLs for the current batch
131 | const batchFileNames = imageFileNames.slice(start, end); // slice the file names for the current batch
132 | for (let j = 0; j < batchFileNames.length; j++) {
133 | if (batchFileNames[j].fullPath === undefined) {
134 | console.error(
135 | `Error: Unable to download ${batchURLs[i]} (fullPath is undefined).`
136 | );
137 | return;
138 | }
139 | }
140 |
141 | const promises = batchURLs.map((url, index) => {
142 | const file = batchFileNames[index];
143 | if (file.fullPath === undefined) {
144 | console.error(
145 | `Error: Unable to download ${url} (fullPath is undefined).`
146 | );
147 | return Promise.resolve();
148 | }
149 |
150 | // check if the image was downloaded before and if it's still valid
151 | // if it's valid, skip downloading it again
152 | // if there is no .lastUpdated file, download the image
153 | // if there is a .lastUpdated file, check if it's older than the image
154 | // if it's older, download the image
155 |
156 | const lastUpdatedFilename = `${file.file}.lastUpdated`;
157 | const lastUpdatedPath = path.join(file.basePath, lastUpdatedFilename);
158 |
159 | let skipDownload = false;
160 | if (fs.existsSync(lastUpdatedPath) && fs.existsSync(file.fullPath)) {
161 | const lastUpdated = fs.readFileSync(lastUpdatedPath, "utf8");
162 | const lastUpdatedTimestamp = parseInt(lastUpdated);
163 | const now = Date.now();
164 | const timeDifferenceInSeconds = (now - lastUpdatedTimestamp) / 1000;
165 |
166 | if (timeDifferenceInSeconds < remoteImageCacheTTL) {
167 | // console.log(
168 | // `Skipping download of ${file.file} because it was downloaded ${timeDifferenceInSeconds} seconds ago.`
169 | // );
170 | skipDownload = true;
171 | cachedImages++;
172 | }
173 | } else {
174 | // console.log("No .lastUpdated file found");
175 | }
176 |
177 | if (skipDownload) {
178 | return Promise.resolve();
179 | }
180 | downloadedImages++;
181 | return downloadImage(url, file.fullPath as string, folder);
182 | }); // create an array of promises for downloading images in the batch
183 |
184 | try {
185 | await Promise.all(promises); // download images in parallel for the current batch
186 | downloadedImages > 0 &&
187 | console.log(
188 | `Downloaded ${downloadedImages} remote image${
189 | downloadedImages > 1 ? "s" : ""
190 | }.`
191 | );
192 | cachedImages > 0 &&
193 | console.log(
194 | `Used ${cachedImages} cached remote image${
195 | cachedImages > 1 ? "s" : ""
196 | }.`
197 | );
198 | } catch (err: any) {
199 | console.error(
200 | `Error: Unable to download remote images (${err.message}).`
201 | );
202 | throw err;
203 | }
204 | }
205 | }
206 |
207 | const storeLastUpdated = (file: ImageObject) => {
208 | // to cache the image locally, we need to know last time it was downloaded
209 | // so we can check if it's still valid
210 | // store a file with the same name as the image, but with a .lastUpdated extension and the timestamp
211 | const lastUpdated = Date.now();
212 |
213 | const lastUpdatedFilename = `${file.file}.lastUpdated`;
214 | try {
215 | fs.writeFileSync(lastUpdatedFilename, lastUpdated.toString());
216 | } catch (error) {
217 | console.error(
218 | `Error writing the cache file for ${lastUpdatedFilename}: `,
219 | error
220 | );
221 | throw error;
222 | }
223 | };
224 |
--------------------------------------------------------------------------------
/src/utils/ensureDirectoryExists.ts:
--------------------------------------------------------------------------------
1 | export {};
2 | const fs = require("fs");
3 | const path = require("path");
4 |
5 | function ensureDirectoryExists(filePath: string) {
6 | const dirName = path.dirname(filePath);
7 | if (fs.existsSync(dirName)) {
8 | return true;
9 | }
10 | ensureDirectoryExists(dirName);
11 | fs.mkdirSync(dirName);
12 | }
13 | module.exports = ensureDirectoryExists;
14 |
--------------------------------------------------------------------------------
/src/utils/getAllFilesAsObject.ts:
--------------------------------------------------------------------------------
1 | import { ImageObject } from "./ImageObject";
2 | const fs = require("fs");
3 | module.exports = function getAllFilesAsObject(
4 | basePath: string,
5 | dirPath: string,
6 | exportFolderName: string,
7 | arrayOfFiles: ImageObject[] = []
8 | ) {
9 | // check if the path is existing
10 | if (fs.existsSync(dirPath)) {
11 | let files = fs.readdirSync(dirPath);
12 |
13 | files.forEach(function (file: string) {
14 | if (
15 | fs.statSync(dirPath + "/" + file).isDirectory() &&
16 | file !== exportFolderName &&
17 | file !== "nextImageExportOptimizer" // default export folder name
18 | ) {
19 | arrayOfFiles = getAllFilesAsObject(
20 | basePath,
21 | dirPath + "/" + file,
22 | exportFolderName,
23 | arrayOfFiles
24 | );
25 | } else {
26 | const dirPathWithoutBasePath = dirPath
27 | .replace(basePath, "") // remove the basePath for later path composition
28 | .replace(/^(\/)/, ""); // remove the first trailing slash if there is one at the first position
29 | arrayOfFiles.push({ basePath, dirPathWithoutBasePath, file });
30 | }
31 | });
32 | }
33 |
34 | return arrayOfFiles;
35 | };
36 |
--------------------------------------------------------------------------------
/src/utils/getHash.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | const { createHash } = require("crypto");
4 |
5 | module.exports = function getHash(items: string[]) {
6 | const hash = createHash("sha256");
7 | for (let item of items) {
8 | if (typeof item === "number") hash.update(String(item));
9 | else {
10 | hash.update(item);
11 | }
12 | }
13 | // See https://en.wikipedia.org/wiki/Base64#Filenames
14 | return hash.digest("base64").replace(/\//g, "-");
15 | };
16 |
--------------------------------------------------------------------------------
/src/utils/getRemoteImageURLs.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import fs from "fs";
3 |
4 | const urlToFilename = require("./urlToFilename");
5 |
6 | export async function getRemoteImageURLs(
7 | remoteImageFileName: string,
8 | nextConfigFolder: string,
9 | folderPathForRemoteImages: string
10 | ) {
11 | let remoteImageURLs: string[] = [];
12 | const remoteImagesFilePath = path.join(nextConfigFolder, remoteImageFileName);
13 |
14 | if (fs.existsSync(remoteImagesFilePath)) {
15 | const remoteOptimizedImages = await require(remoteImagesFilePath);
16 |
17 | remoteImageURLs = remoteOptimizedImages;
18 | }
19 | // Create the filenames for the remote images
20 | const remoteImageFilenames = remoteImageURLs.map((url: string) => {
21 | const encodedURL = urlToFilename(url);
22 |
23 | const filename = path.join(folderPathForRemoteImages, encodedURL);
24 |
25 | return {
26 | basePath: folderPathForRemoteImages,
27 | file: encodedURL,
28 | dirPathWithoutBasePath: "",
29 | fullPath: filename,
30 | };
31 | });
32 |
33 | return { remoteImageFilenames, remoteImageURLs };
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils/urlToFilename.ts:
--------------------------------------------------------------------------------
1 | module.exports = function urlToFilename(url: string) {
2 | try {
3 | const parsedUrl = new URL(url);
4 | const extension = parsedUrl.pathname.split(".").pop();
5 | if (extension) {
6 | return hashAlgorithm(url).toString().concat(".", extension);
7 | }
8 | return hashAlgorithm(url).toString();
9 | } catch (error) {
10 | console.error("Error parsing URL", url, error);
11 | }
12 | };
13 |
14 | // Credits to https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
15 | // This is a hash function that is used to generate a hash from the image URL
16 | const hashAlgorithm = (str: string, seed = 0) => {
17 | let h1 = 0xdeadbeef ^ seed,
18 | h2 = 0x41c6ce57 ^ seed;
19 | for (let i = 0, ch; i < str.length; i++) {
20 | ch = str.charCodeAt(i);
21 | h1 = Math.imul(h1 ^ ch, 2654435761);
22 | h2 = Math.imul(h2 ^ ch, 1597334677);
23 | }
24 | h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
25 | h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
26 | h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
27 | h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
28 |
29 | return 4294967296 * (2097151 & h2) + (h1 >>> 0);
30 | };
31 |
--------------------------------------------------------------------------------
/test/optimizeImages.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | const assert = require("assert");
3 | const fs = require("fs");
4 | const execSync = require("child_process").execSync;
5 | const sharp = require("sharp");
6 |
7 | const deleteFolder = (folderName) => {
8 | if (fs.existsSync(folderName)) {
9 | fs.rmSync(folderName, {
10 | recursive: true,
11 | force: false,
12 | });
13 | }
14 | assert(!fs.existsSync(folderName));
15 | };
16 |
17 | const filterForImages = (file) => {
18 | let extension = file.split(".").pop().toUpperCase();
19 | // Stop if the file is not an image
20 | return ["JPG", "JPEG", "WEBP", "PNG", "GIF", "AVIF"].includes(extension);
21 | };
22 | const getFiles = (dirPath) =>
23 | fs.existsSync(dirPath) ? fs.readdirSync(dirPath) : [];
24 |
25 | const legacyConfig = `module.exports = {
26 | images: {
27 | loader: "custom",
28 | imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
29 | deviceSizes: [640, 750, 777, 828, 1080, 1200, 1920, 2048, 3840],
30 | },
31 | output: "export",
32 | transpilePackages: ["next-image-export-optimizer"],
33 | env: {
34 | storePicturesInWEBP: "false",
35 | generateAndUseBlurImages: "true",
36 | },
37 | };
38 | `;
39 | const newConfig = `module.exports = {
40 | images: {
41 | loader: "custom",
42 | imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
43 | deviceSizes: [640, 750, 777, 828, 1080, 1200, 1920, 2048, 3840],
44 | },
45 | output: "export",
46 | transpilePackages: ["next-image-export-optimizer"],
47 | env: {
48 | nextImageExportOptimizer_imageFolderPath: "public/images",
49 | nextImageExportOptimizer_exportFolderPath: "out",
50 | nextImageExportOptimizer_exportFolderName: "nextImageExportOptimizer",
51 | nextImageExportOptimizer_quality: "75",
52 | nextImageExportOptimizer_storePicturesInWEBP: "true",
53 | nextImageExportOptimizer_generateAndUseBlurImages: "true",
54 | nextImageExportOptimizer_remoteImageCacheTTL: "0",
55 | },
56 | };
57 | `;
58 | const newConfigJpeg = `module.exports = {
59 | images: {
60 | loader: "custom",
61 | imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
62 | deviceSizes: [640, 750, 777, 828, 1080, 1200, 1920, 2048, 3840],
63 | },
64 | output: "export",
65 | transpilePackages: ["next-image-export-optimizer"],
66 | env: {
67 | nextImageExportOptimizer_imageFolderPath: "public/images",
68 | nextImageExportOptimizer_exportFolderPath: "out",
69 | nextImageExportOptimizer_quality: "75",
70 | nextImageExportOptimizer_storePicturesInWEBP: "false",
71 | nextImageExportOptimizer_generateAndUseBlurImages: "true",
72 | nextImageExportOptimizer_remoteImageCacheTTL: "0",
73 | },
74 | };
75 | `;
76 | const newConfigExportFolderName = `module.exports = {
77 | images: {
78 | loader: "custom",
79 | imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
80 | deviceSizes: [640, 750, 777, 828, 1080, 1200, 1920, 2048, 3840],
81 | },
82 | output: "export",
83 | transpilePackages: ["next-image-export-optimizer"],
84 | env: {
85 | nextImageExportOptimizer_imageFolderPath: "public/images",
86 | nextImageExportOptimizer_exportFolderPath: "out",
87 | nextImageExportOptimizer_quality: "75",
88 | nextImageExportOptimizer_storePicturesInWEBP: "false",
89 | nextImageExportOptimizer_generateAndUseBlurImages: "true",
90 | nextImageExportOptimizer_exportFolderName: "nextImageExportOptimizer2",
91 | },
92 | };
93 | `;
94 |
95 | const newConfigBasePath = `module.exports = {
96 | basePath: "/subsite",
97 | images: {
98 | loader: "custom",
99 | imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
100 | deviceSizes: [640, 750, 777, 828, 1080, 1200, 1920, 2048, 3840],
101 | },
102 | transpilePackages: ["next-image-export-optimizer"],
103 | env: {
104 | nextImageExportOptimizer_imageFolderPath: "public/images",
105 | nextImageExportOptimizer_exportFolderPath: "out",
106 | nextImageExportOptimizer_exportFolderName: "nextImageExportOptimizer",
107 | nextImageExportOptimizer_quality: "75",
108 | nextImageExportOptimizer_storePicturesInWEBP: "true",
109 | nextImageExportOptimizer_generateAndUseBlurImages: "true",
110 | nextImageExportOptimizer_remoteImageCacheTTL: "0",
111 | },
112 | };
113 | `;
114 |
115 | async function testConfig(config) {
116 | deleteFolder("example/public/images/nextImageExportOptimizer");
117 | deleteFolder("example/public/nextImageExportOptimizer");
118 | deleteFolder("example/public/images/subfolder/nextImageExportOptimizer");
119 | deleteFolder(
120 | "example/public/images/subfolder/subfolder2/nextImageExportOptimizer"
121 | );
122 | deleteFolder("example/public/images/nextImageExportOptimizer2");
123 | deleteFolder("example/public/nextImageExportOptimizer2");
124 | deleteFolder("example/public/images/subfolder/nextImageExportOptimizer2");
125 | deleteFolder(
126 | "example/public/images/subfolder/subfolder2/nextImageExportOptimizer2"
127 | );
128 | // write config file for the to be tested configuration variables to the folder
129 | fs.writeFileSync("example/next.config.ts", config);
130 |
131 | deleteFolder("example/out/images/nextImageExportOptimizer");
132 | deleteFolder("example/out/nextImageExportOptimizer");
133 | deleteFolder("example/out/images/subfolder/nextImageExportOptimizer");
134 | deleteFolder(
135 | "example/out/images/subfolder/subfolder2/nextImageExportOptimizer"
136 | );
137 | deleteFolder("example/out/images/nextImageExportOptimizer2");
138 | deleteFolder("example/out/nextImageExportOptimizer2");
139 | deleteFolder("example/out/images/subfolder/nextImageExportOptimizer2");
140 | deleteFolder(
141 | "example/out/images/subfolder/subfolder2/nextImageExportOptimizer2"
142 | );
143 |
144 | execSync(
145 | "npm run build && cd example/ && npm run export && node ../dist/optimizeImages.js"
146 | );
147 |
148 | const allFilesInImageFolder = getFiles(
149 | "example/public/images/nextImageExportOptimizer"
150 | );
151 | const allImagesInImageFolder = allFilesInImageFolder.filter(filterForImages);
152 | const allFilesInStaticImageFolder = getFiles(
153 | "example/public/nextImageExportOptimizer"
154 | );
155 | const allImagesInStaticImageFolder =
156 | allFilesInStaticImageFolder.filter(filterForImages);
157 |
158 | const allFilesInImageSubFolder = getFiles(
159 | "example/public/images/subfolder/nextImageExportOptimizer"
160 | );
161 | const allImagesInImageSubFolder =
162 | allFilesInImageSubFolder.filter(filterForImages);
163 |
164 | const allFilesInImageBuildFolder = getFiles(
165 | "example/out/images/nextImageExportOptimizer"
166 | );
167 | const allFilesInStaticImageBuildFolder = getFiles(
168 | "example/out/nextImageExportOptimizer"
169 | );
170 |
171 | const allFilesInImageBuildSubFolder = getFiles(
172 | "example/out/images/subfolder/nextImageExportOptimizer"
173 | );
174 |
175 | // For custom export folder name
176 | const allFilesInImageFolderCustomExportFolder = getFiles(
177 | "example/public/images/nextImageExportOptimizer2"
178 | );
179 |
180 | const allImagesInImageFolderCustomExportFolder =
181 | allFilesInImageFolderCustomExportFolder.filter(filterForImages);
182 | const allFilesInStaticImageFolderCustomExportFolder = getFiles(
183 | "example/public/nextImageExportOptimizer2"
184 | );
185 |
186 | const allImagesInStaticImageFolderCustomExportFolder =
187 | allFilesInStaticImageFolderCustomExportFolder.filter(filterForImages);
188 |
189 | const allFilesInImageSubFolderCustomExportFolder = getFiles(
190 | "example/public/images/subfolder/nextImageExportOptimizer2"
191 | );
192 |
193 | const allImagesInImageSubFolderCustomExportFolder =
194 | allFilesInImageSubFolderCustomExportFolder.filter(filterForImages);
195 |
196 | const allFilesInImageBuildFolderCustomExportFolder = getFiles(
197 | "example/out/images/nextImageExportOptimizer2"
198 | );
199 |
200 | const allFilesInStaticImageBuildFolderCustomExportFolder = getFiles(
201 | "example/out/nextImageExportOptimizer2"
202 | );
203 |
204 | const allFilesInImageBuildSubFolderCustomExportFolder = getFiles(
205 | "example/out/images/subfolder/nextImageExportOptimizer2"
206 | );
207 |
208 | if (
209 | config === newConfig ||
210 | config === legacyConfig ||
211 | config === newConfigBasePath
212 | ) {
213 | expect(allImagesInImageFolder).toMatchSnapshot();
214 | expect(allImagesInStaticImageFolder).toMatchSnapshot();
215 |
216 | expect(allImagesInImageSubFolder).toMatchSnapshot();
217 | expect(allFilesInImageBuildFolder).toMatchSnapshot();
218 | expect(allFilesInStaticImageFolder).toMatchSnapshot();
219 | expect(allFilesInImageBuildSubFolder).toMatchSnapshot();
220 | } else if (config === newConfigExportFolderName) {
221 | expect(allImagesInImageFolderCustomExportFolder).toMatchSnapshot();
222 | expect(allImagesInStaticImageFolderCustomExportFolder).toMatchSnapshot();
223 | expect(allImagesInImageSubFolderCustomExportFolder).toMatchSnapshot();
224 | expect(allFilesInImageBuildFolderCustomExportFolder).toMatchSnapshot();
225 | expect(
226 | allFilesInStaticImageBuildFolderCustomExportFolder
227 | ).toMatchSnapshot();
228 | expect(allFilesInImageBuildSubFolderCustomExportFolder).toMatchSnapshot();
229 | } else {
230 | expect(allImagesInImageFolder).toMatchSnapshot();
231 | expect(allImagesInStaticImageFolder).toMatchSnapshot();
232 | expect(allImagesInImageSubFolder).toMatchSnapshot();
233 | expect(allFilesInImageBuildFolder).toMatchSnapshot();
234 | expect(allFilesInStaticImageBuildFolder).toMatchSnapshot();
235 | expect(allFilesInImageBuildSubFolder).toMatchSnapshot();
236 | }
237 |
238 | const imageFolders = [
239 | {
240 | basePath: "example/public/images/nextImageExportOptimizer",
241 | imageFileArray: allImagesInImageFolder,
242 | },
243 | {
244 | basePath: "example/public/images/subfolder/nextImageExportOptimizer",
245 | imageFileArray: allFilesInImageBuildSubFolder,
246 | },
247 | {
248 | basePath: "example/public/nextImageExportOptimizer",
249 | imageFileArray: allImagesInStaticImageFolder,
250 | },
251 | {
252 | basePath: "example/public/images/nextImageExportOptimizer2",
253 | imageFileArray: allImagesInImageFolderCustomExportFolder,
254 | },
255 | {
256 | basePath: "example/public/images/subfolder/nextImageExportOptimizer2",
257 | imageFileArray: allFilesInImageBuildSubFolderCustomExportFolder,
258 | },
259 | {
260 | basePath: "example/public/nextImageExportOptimizer2",
261 | imageFileArray: allImagesInStaticImageFolderCustomExportFolder,
262 | },
263 | ];
264 | for (let index = 0; index < imageFolders.length; index++) {
265 | const imageFolderBasePath = imageFolders[index].basePath;
266 | const imageFileArray = imageFolders[index].imageFileArray;
267 |
268 | const imageFileStats = [];
269 | for (let index = 0; index < imageFileArray.length; index++) {
270 | const imageFile = imageFileArray[index];
271 | const image = await sharp(`${imageFolderBasePath}/${imageFile}`);
272 | const metadata = await image.metadata();
273 | const statsToBeChecked = [
274 | metadata.format,
275 | metadata.width,
276 | metadata.height,
277 | ];
278 | imageFileStats.push(statsToBeChecked);
279 | }
280 | if (
281 | config === newConfig ||
282 | config === legacyConfig ||
283 | config === newConfigBasePath
284 | ) {
285 | if (index == 0 || index == 2) {
286 | expect(imageFileStats).toMatchSnapshot();
287 | } else if (index === 1) {
288 | expect(imageFileStats).toMatchSnapshot();
289 | }
290 | }
291 | if (config === newConfigJpeg) {
292 | if (index == 0 || index == 2) {
293 | expect(imageFileStats).toMatchSnapshot();
294 | } else if (index === 1) {
295 | expect(imageFileStats).toMatchSnapshot();
296 | }
297 | }
298 | }
299 | }
300 |
301 | jest.setTimeout(180000);
302 | test("legacyConfig", async () => {
303 | console.log("Running legacyConfig test...");
304 | await testConfig(legacyConfig);
305 | console.log("legacyConfig test finished.");
306 | });
307 |
308 | test("newConfigJpeg", async () => {
309 | console.log("Running newConfigJpeg test...");
310 | await testConfig(newConfigJpeg);
311 | console.log("newConfigJpeg test finished.");
312 | });
313 |
314 | test("newConfigExportFolderName", async () => {
315 | console.log("Running newConfigExportFolderName test...");
316 | await testConfig(newConfigExportFolderName);
317 | console.log("newConfigExportFolderName test finished.");
318 | });
319 |
320 | test("newConfigBasePath", async () => {
321 | console.log("Running newConfigBasePath test...");
322 | await testConfig(newConfigBasePath);
323 | console.log("newConfigBasePath test finished.");
324 | });
325 |
326 | test("newConfig", async () => {
327 | console.log("Running newConfig test...");
328 | await testConfig(newConfig);
329 | console.log("newConfig test finished.");
330 | });
331 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Enable incremental compilation */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | "jsx": "react" /* Specify what JSX code is generated. */,
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 |
26 | /* Modules */
27 | "module": "ES2022" /* Specify what module code is generated. */,
28 | // "rootDir": "./src" /* Specify the root folder within your source files. */,
29 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
36 | // "resolveJsonModule": true, /* Enable importing .json files */
37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
38 |
39 | /* JavaScript Support */
40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
43 |
44 | /* Emit */
45 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
47 | "emitDeclarationOnly": false /* Only output d.ts files and not JavaScript files. */,
48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
49 | // "outFile": "./dist/index.js" /* 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. */,
50 | "outDir": "./dist" /* Specify an output folder for all emitted files. */,
51 | // "removeComments": true, /* Disable emitting comments. */
52 | // "noEmit": true, /* Disable emitting files from a compilation. */
53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
61 | // "newLine": "crlf", /* Set the newline character for emitting files. */
62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
66 | // "declarationDir": "./dist" /* Specify the output directory for generated declaration files. */,
67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
68 |
69 | /* Interop Constraints */
70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
72 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
74 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
75 |
76 | /* Type Checking */
77 | "strict": true /* Enable all strict type-checking options. */,
78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
96 |
97 | /* Completeness */
98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
100 | },
101 | "exclude": [
102 | "node_modules",
103 | "example",
104 | "dist",
105 | "src/optimizeImages.ts",
106 | "src/utils"
107 | ],
108 | "files": [
109 | "example/src/ExportedImage.tsx",
110 | "example/src/legacy/ExportedImage.tsx",
111 | "example/environment.d.ts"
112 | ]
113 | }
114 |
--------------------------------------------------------------------------------
/tsconfig.optimizeImages.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Enable incremental compilation */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "react" /* Specify what JSX code is generated. */,
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 |
26 | /* Modules */
27 | "module": "CommonJS" /* Specify what module code is generated. */,
28 | // "rootDir": "./src" /* Specify the root folder within your source files. */,
29 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
36 | // "resolveJsonModule": true, /* Enable importing .json files */
37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
38 |
39 | /* JavaScript Support */
40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
43 |
44 | /* Emit */
45 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
47 | "emitDeclarationOnly": false /* Only output d.ts files and not JavaScript files. */,
48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
49 | // "outFile": "./dist/index.js" /* 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. */,
50 | "outDir": "./dist" /* Specify an output folder for all emitted files. */,
51 | // "removeComments": true, /* Disable emitting comments. */
52 | // "noEmit": true, /* Disable emitting files from a compilation. */
53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
61 | // "newLine": "crlf", /* Set the newline character for emitting files. */
62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
66 | // "declarationDir": "./dist" /* Specify the output directory for generated declaration files. */,
67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
68 |
69 | /* Interop Constraints */
70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
72 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
74 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
75 |
76 | /* Type Checking */
77 | "strict": true /* Enable all strict type-checking options. */,
78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
96 |
97 | /* Completeness */
98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
100 | },
101 | "exclude": [
102 | "node_modules",
103 | "example",
104 | "dist",
105 | "src/legacy",
106 | "src/ExportedImage.tsx"
107 | ]
108 | }
109 |
--------------------------------------------------------------------------------