├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql-analysis.yml │ ├── node.js.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── .tool-versions ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ ├── cli │ ├── cache │ │ ├── fixtures │ │ │ └── default.png │ │ └── index.test.ts │ ├── external-images │ │ ├── index.test.ts │ │ └── manifest.json │ ├── image-optimize │ │ ├── fixtures │ │ │ ├── default.avif │ │ │ ├── default.jpeg │ │ │ ├── default.jpg │ │ │ ├── default.png │ │ │ ├── default.svg │ │ │ └── default.webp │ │ ├── index.test.ts │ │ └── manifest.json │ └── uniqby │ │ └── index.test.ts ├── e2e-build │ ├── app │ │ ├── layout.jsx │ │ └── page.jsx │ ├── cli.js │ ├── export-images.config.js │ ├── index.test.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── public │ │ └── images │ │ │ └── img.png │ └── src │ │ └── images │ │ └── img.png ├── e2e │ ├── app │ │ ├── layout.jsx │ │ └── page.jsx │ ├── cli.js │ ├── components │ │ ├── ClientComponent.jsx │ │ └── WithPropsComponent.jsx │ ├── export-images.config.js │ ├── images │ │ ├── client-only.png │ │ ├── get-props-mobile.png │ │ ├── get-props.png │ │ ├── img.png │ │ ├── legacy-img.png │ │ └── picture.png │ ├── index.test.ts │ ├── next-env.d.ts │ ├── next.config.js │ └── public │ │ └── images │ │ ├── animated.webp │ │ ├── ignore-img.png │ │ ├── img.png │ │ ├── img.svg │ │ ├── legacy-img.png │ │ └── picture.png └── utils │ └── buildOutputInfo │ └── index.test.ts ├── bin └── index.js ├── biome.json ├── changelog.config.js ├── commit-types.config.js ├── commitlint.config.js ├── docs ├── .gitignore ├── .prettierrc.js ├── .tool-versions ├── README.md ├── babel.config.js ├── docs │ ├── 01-intro.md │ ├── 02-getting-started.md │ ├── 03-Features │ │ ├── 01-picture-component.md │ │ ├── 02-remote-image-component.md │ │ ├── 03-build-mode.md │ │ ├── 04-get-props.md │ │ ├── 05-external-images.md │ │ └── _category_.json │ ├── 04-Configurations │ │ ├── 01-basic-configuration.md │ │ └── _category_.json │ ├── 05-comparison.md │ ├── 06-examples.md │ ├── 07-qa.md │ └── 08-planned-features.md ├── docusaurus.config.js ├── package-lock.json ├── package.json ├── sidebars.js ├── src │ ├── components │ │ ├── Features.jsx │ │ ├── Hero.jsx │ │ ├── Introduction.jsx │ │ └── Text.jsx │ ├── css │ │ └── custom.css │ └── pages │ │ └── index.jsx ├── static │ └── og.png └── tailwind.config.js ├── image.d.ts ├── image.js ├── jest.config.js ├── legacy ├── image.d.ts └── image.js ├── package-lock.json ├── package.json ├── picture.d.ts ├── picture.js ├── release.config.js ├── remote-image.d.ts ├── remote-image.js ├── remote-picture.d.ts ├── remote-picture.js ├── renovate.json ├── src ├── cli │ ├── external-images │ │ └── index.ts │ ├── index.ts │ └── utils │ │ ├── cache.ts │ │ ├── cliProgressBar.ts │ │ └── uniqueItems.ts ├── components │ ├── client │ │ ├── image.tsx │ │ ├── legacy │ │ │ └── image.tsx │ │ └── picture.tsx │ ├── server │ │ ├── remote-image.tsx │ │ └── remote-picture.tsx │ └── utils │ │ ├── getOptimizedImageProps.ts │ │ ├── getStringSrc.ts │ │ └── imageLoader.ts ├── index.ts ├── loader │ └── index.ts ├── utils │ ├── buildOutputInfo.ts │ ├── formatValidate.ts │ ├── getConfig.ts │ ├── parseNdJSON.ts │ └── processManifest.ts └── withExportImages.ts ├── tsconfig.json └── tsup.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: dc7290 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | # How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 19 | 20 | # Checklist: 21 | 22 | - [ ] My code follows the style guidelines of this project 23 | - [ ] I have performed a self-review of my own code 24 | - [ ] I have commented my code, particularly in hard-to-understand areas 25 | - [ ] I have made corresponding changes to the documentation 26 | - [ ] My changes generate no new warnings 27 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '38 21 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | node: 14 | - 18 15 | - 20 16 | - 22 17 | os: 18 | - ubuntu-latest 19 | name: Node.js v${{ matrix.node }} on ${{ matrix.os }} 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Use Node.js ${{ matrix.node }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node }} 27 | cache: 'npm' 28 | - run: npm ci 29 | - run: npm run test 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [release, beta] 6 | workflow_dispatch: {} 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Use Node.js 22 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 22 17 | cache: 'npm' 18 | - run: npm ci 19 | - run: npm run build 20 | - name: Release 21 | env: 22 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | GIT_AUTHOR_NAME: 'dc7290' 25 | GIT_AUTHOR_EMAIL: 'dhkh.cba0927@gmail.com' 26 | GIT_COMMITTER_NAME: 'dc7290' 27 | GIT_COMMITTER_EMAIL: 'dhkh.cba0927@gmail.com' 28 | run: npm run semantic-release 29 | -------------------------------------------------------------------------------- /.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 | # production 9 | dist 10 | 11 | # testing 12 | /coverage 13 | /__tests__/cli/image-optimize/fixtures/results 14 | /__tests__/cli/cache/.cache 15 | /__tests__/cli/cache/results 16 | /__tests__/cli/external-images/fixtures 17 | /__tests__/components/image/manifest/manifest.json 18 | /__tests__/components/image/external-images/manifest.json 19 | 20 | # benchmark 21 | /bench/fixtures/results 22 | 23 | # next.js 24 | .next 25 | out 26 | 27 | # misc 28 | .DS_Store 29 | 30 | # debug 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | 35 | # custom 36 | .eslintcache 37 | tsconfig.*tsbuildinfo 38 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npm run commitmsg 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint-staged 2 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | [ -z "${SKIP_BY_SEMANTIC_RELEASE}" ] && { 2 | exec < /dev/tty && npx git-cz --hook 3 | } || true 4 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 22.16.0 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "quickfix.biome": "explicit", 5 | "source.organizeImports.biome": "explicit" 6 | }, 7 | "editor.defaultFormatter": "biomejs.biome", 8 | "[javascript]": { 9 | "editor.defaultFormatter": "biomejs.biome" 10 | }, 11 | "[typescript]": { 12 | "editor.defaultFormatter": "biomejs.biome" 13 | }, 14 | "[json]": { 15 | "editor.defaultFormatter": "biomejs.biome" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/). 6 | 7 | ## [Released](https://github.com/dc7290/next-export-optimize-images/releases) 8 | 9 | ## [4.6.2](https://github.com/dc7290/next-export-optimize-images/compare/v4.6.1...v4.6.2) (2025-03-20) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * 🐛 Don't log spam configuration loaded log ([8e4ab14](https://github.com/dc7290/next-export-optimize-images/commit/8e4ab1457cc28ade1ebbe10d1ba01877257ca79b)) 15 | * **deps:** update dependency fs-extra to ^11.3.0 ([#1129](https://github.com/dc7290/next-export-optimize-images/issues/1129)) ([77b67c3](https://github.com/dc7290/next-export-optimize-images/commit/77b67c30a858a50090d5414097a3d5f293c8d82a)) 16 | 17 | 18 | ### Documentation 19 | 20 | * ✏️ add examples for using mixed ([74df8a9](https://github.com/dc7290/next-export-optimize-images/commit/74df8a9099ea9b008b9f5a50fc756666bcca2caf)) 21 | * ✏️ Update docusaurus v3 ([55143dc](https://github.com/dc7290/next-export-optimize-images/commit/55143dc8991c7cecb93cd73be9a7435bf434f9ef)) 22 | 23 | 24 | ### Tests 25 | 26 | * 💍 Added tests for the Pocture component and deviceSizes ([c82903b](https://github.com/dc7290/next-export-optimize-images/commit/c82903b589869534b52141ebb7da40a7027b4be1)) 27 | 28 | ## [4.6.1](https://github.com/dc7290/next-export-optimize-images/compare/v4.6.0...v4.6.1) (2024-12-12) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * 🐛 Fixed dts file output ([052d953](https://github.com/dc7290/next-export-optimize-images/commit/052d953f9936aa5ddd4cffcccac9cd582d2a5958)) 34 | * 🐛 Support Node.js v22 ([ec96198](https://github.com/dc7290/next-export-optimize-images/commit/ec96198d9f7bfe78c0895762bbd2a488dc84e3a2)) 35 | 36 | 37 | ### Continuous Integration 38 | 39 | * 🎡 Fixed matrix os ([5153978](https://github.com/dc7290/next-export-optimize-images/commit/5153978252485b6f31972fe1cb0d0f17e1aa6916)) 40 | 41 | ## [4.6.0](https://github.com/dc7290/next-export-optimize-images/compare/v4.5.3...v4.6.0) (2024-12-10) 42 | 43 | 44 | ### Features 45 | 46 | * 🚀 Add `ignorePaths` option ([720c8d5](https://github.com/dc7290/next-export-optimize-images/commit/720c8d5ea41982d38430f032eecd75c2288c4d1e)), closes [#1076](https://github.com/dc7290/next-export-optimize-images/issues/1076) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * 🐛 Fix for public directory bug in build mode ([b7852b4](https://github.com/dc7290/next-export-optimize-images/commit/b7852b4c6682324d8947ec935e77215b8983dfd7)), closes [#1086](https://github.com/dc7290/next-export-optimize-images/issues/1086) 52 | * 🐛 Fixed tsup build order ([7d59322](https://github.com/dc7290/next-export-optimize-images/commit/7d593227b0993e88559a435e8b31c60f0e09c646)), closes [#981](https://github.com/dc7290/next-export-optimize-images/issues/981) 53 | 54 | 55 | ### Tests 56 | 57 | * 💍 Restore test-image ([67bb9d9](https://github.com/dc7290/next-export-optimize-images/commit/67bb9d9c5beb468f5e47483b936c481cfb9c232d)) 58 | 59 | ## [4.5.3](https://github.com/dc7290/next-export-optimize-images/compare/v4.5.2...v4.5.3) (2024-11-29) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * 🐛 Fixed syntax for peerDependencies ([0a2574b](https://github.com/dc7290/next-export-optimize-images/commit/0a2574bde232f3ba25b51cd743cb1be3896dab17)), closes [#1093](https://github.com/dc7290/next-export-optimize-images/issues/1093) 65 | 66 | ## [4.5.2](https://github.com/dc7290/next-export-optimize-images/compare/v4.5.1...v4.5.2) (2024-11-12) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * 🐛 Fixing Devdependency Versions ([7ab0a1f](https://github.com/dc7290/next-export-optimize-images/commit/7ab0a1f75c14c41a472bb21b02d2c46cf83ee9cd)) 72 | * 🐛 Generate d.ts ([531bc72](https://github.com/dc7290/next-export-optimize-images/commit/531bc725df84964b1dd67f8dc7f4e881aa2ec340)), closes [#981](https://github.com/dc7290/next-export-optimize-images/issues/981) 73 | * 🐛 Support Next.js 15 and React19RC ([701ffb8](https://github.com/dc7290/next-export-optimize-images/commit/701ffb8ab1cb667d6be9dddd374c9e2411aa1c4e)) 74 | 75 | 76 | ### Documentation 77 | 78 | * ✏️ Added comparison and esm config ([3e7af87](https://github.com/dc7290/next-export-optimize-images/commit/3e7af870f51728361e970f30d924c984a4e5c4a3)) 79 | 80 | 81 | ### Continuous Integration 82 | 83 | * 🎡 Add workflow_dispatch in Release-workflow ([316a5f1](https://github.com/dc7290/next-export-optimize-images/commit/316a5f15991bb1c7d3c61c48054c865d69c78832)) 84 | 85 | ## [4.5.1](https://github.com/dc7290/next-export-optimize-images/compare/v4.5.0...v4.5.1) (2024-10-07) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * 🐛 Apply loader: custom to image settings in Next.js ([a9c35b1](https://github.com/dc7290/next-export-optimize-images/commit/a9c35b1061c8dd2a388c26a9f76f8694c8716b93)), closes [#971](https://github.com/dc7290/next-export-optimize-images/issues/971) 91 | * 🐛 Fix for ENOENT No such file or directory ([e425220](https://github.com/dc7290/next-export-optimize-images/commit/e42522073f58232d0757568f0591c7b3f0f9f73d)), closes [#945](https://github.com/dc7290/next-export-optimize-images/issues/945) 92 | * **deps:** update dependency sharp to ^0.33.5 ([359a635](https://github.com/dc7290/next-export-optimize-images/commit/359a635489e2866c9244b0524af59157b62a6cb5)) 93 | 94 | 95 | ### Documentation 96 | 97 | * ✏️ Added notes ([d44fd70](https://github.com/dc7290/next-export-optimize-images/commit/d44fd70b43bcb06c4ee184c6801f3ef6a88a056b)) 98 | 99 | ## [4.5.0](https://github.com/dc7290/next-export-optimize-images/compare/v4.4.0...v4.5.0) (2024-06-09) 100 | 101 | 102 | ### Features 103 | 104 | * 🚀 Support `.cjs` ([4e58bdb](https://github.com/dc7290/next-export-optimize-images/commit/4e58bdb061766720c4a9317587015ed66b6a671d)) 105 | 106 | 107 | ### Documentation 108 | 109 | * ✏️ Handling page swaps and relative paths ([6d86e34](https://github.com/dc7290/next-export-optimize-images/commit/6d86e3405f2cde1614552c53ac70c046425c2939)) 110 | 111 | ## [4.4.0](https://github.com/dc7290/next-export-optimize-images/compare/v4.3.1...v4.4.0) (2024-05-19) 112 | 113 | 114 | ### Features 115 | 116 | * 🚀 Add RemoteImage component ([c4b284d](https://github.com/dc7290/next-export-optimize-images/commit/c4b284de4d0ca05657de14ea3263b71c41d1f447)), closes [#652](https://github.com/dc7290/next-export-optimize-images/issues/652) 117 | * 🚀 Add RemotePicture component ([3d4d1d2](https://github.com/dc7290/next-export-optimize-images/commit/3d4d1d2892d21899bcb4250a3bf5d56ec8d074d0)) 118 | 119 | 120 | ### Bug Fixes 121 | 122 | * **deps:** update dependency sharp to ^0.33.4 ([a0a0fa7](https://github.com/dc7290/next-export-optimize-images/commit/a0a0fa718b8c57c7501da985656aa2e42483186c)) 123 | 124 | 125 | ### Documentation 126 | 127 | * ✏️ Add case for using another plugin ([3d9e988](https://github.com/dc7290/next-export-optimize-images/commit/3d9e988bd24ef9c31806891d5f731593894e0ad6)) 128 | 129 | ## [4.3.1](https://github.com/dc7290/next-export-optimize-images/compare/v4.3.0...v4.3.1) (2024-04-14) 130 | 131 | 132 | ### Bug Fixes 133 | 134 | * 🐛 Enable Node.js-specific APIs in the configuration file ([f5a2805](https://github.com/dc7290/next-export-optimize-images/commit/f5a2805b39a6c886ca73c49b2778818f8b36479c)), closes [#824](https://github.com/dc7290/next-export-optimize-images/issues/824) 135 | 136 | 137 | ### Documentation 138 | 139 | * ✏️ Fixed filenameGenerator arguments ([4aebf7a](https://github.com/dc7290/next-export-optimize-images/commit/4aebf7ac78d4257eb79622f60fe39b7e0a6f4496)) 140 | 141 | ## [4.3.0](https://github.com/dc7290/next-export-optimize-images/compare/v4.2.0...v4.3.0) (2024-04-13) 142 | 143 | 144 | ### Features 145 | 146 | * 🚀 Support `next start` ([dfb746a](https://github.com/dc7290/next-export-optimize-images/commit/dfb746ad3c812a3cd1a0f24373bee3ba71729a42)), closes [#105](https://github.com/dc7290/next-export-optimize-images/issues/105) 147 | 148 | 149 | ### Performance Improvements 150 | 151 | * ⚡️ Minimize readFile execution ([139f459](https://github.com/dc7290/next-export-optimize-images/commit/139f459132b8b174ee8ba2dc67d6816bf618ccfa)), closes [#742](https://github.com/dc7290/next-export-optimize-images/issues/742) 152 | 153 | 154 | ### Tests 155 | 156 | * 💍 Fixed test cases ([b9e21ea](https://github.com/dc7290/next-export-optimize-images/commit/b9e21ea10953f998a4e93a168b4c4b32df8b06b2)) 157 | 158 | ## [4.2.0](https://github.com/dc7290/next-export-optimize-images/compare/v4.1.0...v4.2.0) (2024-04-13) 159 | 160 | 161 | ### Features 162 | 163 | * 🚀 New feature getOptimizedImageProps implemented ([96342af](https://github.com/dc7290/next-export-optimize-images/commit/96342afee055c0c3465d2f5e17e9a706f2abea9f)), closes [#822](https://github.com/dc7290/next-export-optimize-images/issues/822) 164 | 165 | 166 | ### Bug Fixes 167 | 168 | * 🐛 Corrected attribute values for the source element ([e91cb59](https://github.com/dc7290/next-export-optimize-images/commit/e91cb5919c4ad226612be4475ac3040b769ba889)) 169 | 170 | ## [4.1.0](https://github.com/dc7290/next-export-optimize-images/compare/v4.0.0...v4.1.0) (2024-04-12) 171 | 172 | 173 | ### Features 174 | 175 | * 🚀 allow changing cacheDir location ([2215bb7](https://github.com/dc7290/next-export-optimize-images/commit/2215bb7f15bd3f7100744ee2a39fd5e6a28b61f8)), closes [#692](https://github.com/dc7290/next-export-optimize-images/issues/692) 176 | * 🚀 Applied additional attributes to Picture Component ([1f8866d](https://github.com/dc7290/next-export-optimize-images/commit/1f8866d1cad350e45ba60baa9214da528cf2749c)), closes [#693](https://github.com/dc7290/next-export-optimize-images/issues/693) 177 | 178 | 179 | ### Bug Fixes 180 | 181 | * 🐛 Enables next/image to be used in conjunction with ([5e82ad7](https://github.com/dc7290/next-export-optimize-images/commit/5e82ad7cbe1094d62bc5e9057554861f3170ffe9)) 182 | * 🐛 Update sharp to v0.33.3 ([a2d6fa7](https://github.com/dc7290/next-export-optimize-images/commit/a2d6fa7c4b5656b4e575feeb7da713bb0e6fa9d8)), closes [#733](https://github.com/dc7290/next-export-optimize-images/issues/733) 183 | 184 | 185 | ### Tests 186 | 187 | * 💍 Add cacheDir config option ([6135ed1](https://github.com/dc7290/next-export-optimize-images/commit/6135ed1e62e64855c581ed03dc9e1026cb5df78d)) 188 | 189 | ## [4.0.0](https://github.com/dc7290/next-export-optimize-images/compare/v3.3.0...v4.0.0) (2024-04-10) 190 | 191 | 192 | ### ⚠ BREAKING CHANGES 193 | 194 | * 🧨The minimum version of Next.js is 14.1.0 195 | * 🧨The import destination changes, no longer from next/image, but from 196 | next-export-optimize-images/image. 197 | * 🧨Node.js v16 has been dropped 198 | 199 | ### Features 200 | 201 | * 🚀 Change import options ([9027b63](https://github.com/dc7290/next-export-optimize-images/commit/9027b63b31b15b4280b1e3c3a23cfb6060fbf964)), closes [#820](https://github.com/dc7290/next-export-optimize-images/issues/820) [#696](https://github.com/dc7290/next-export-optimize-images/issues/696) [#796](https://github.com/dc7290/next-export-optimize-images/issues/796) 202 | * 🚀 Node.js minimum version to 18 ([202db6d](https://github.com/dc7290/next-export-optimize-images/commit/202db6d2cd06244254f40a3eabc558548f7304ba)) 203 | * 🚀 Support Next.js v14 ([ae716a6](https://github.com/dc7290/next-export-optimize-images/commit/ae716a6f9a39a74a45de351d6458e90df77901f8)), closes [#802](https://github.com/dc7290/next-export-optimize-images/issues/802) 204 | 205 | 206 | ### Bug Fixes 207 | 208 | * 🐛 Revive module.exports ([8911e11](https://github.com/dc7290/next-export-optimize-images/commit/8911e11d2c7b54449fe24495ee2c691f841aabb7)) 209 | 210 | 211 | ### Continuous Integration 212 | 213 | * 🎡 Fixed a problem with Release ending in the middle of a re ([9e6c6af](https://github.com/dc7290/next-export-optimize-images/commit/9e6c6af9ce9aff0ca6862a67d4197e771d53dc11)) 214 | 215 | ## [3.3.0](https://github.com/dc7290/next-export-optimize-images/compare/v3.2.0...v3.3.0) (2024-03-10) 216 | 217 | 218 | ### Features 219 | 220 | * 🚀 feat: add config for remote images download delays ([fb05eda](https://github.com/dc7290/next-export-optimize-images/commit/fb05edaeb73b85fb2ac930c30e061c0c0078d6ca)) 221 | 222 | 223 | ### Bug Fixes 224 | 225 | * **deps:** update dependency fs-extra to ^11.2.0 ([83990ed](https://github.com/dc7290/next-export-optimize-images/commit/83990ed601450ad37ec4a99cfa3d1bb6480022b5)) 226 | * **deps:** update dependency sharp to ^0.32.6 ([ea1b24f](https://github.com/dc7290/next-export-optimize-images/commit/ea1b24f4eb3301f3332e97381ee618590b822887)) 227 | * **deps:** update dependency sharp to v0.32.6 [security] ([fed526e](https://github.com/dc7290/next-export-optimize-images/commit/fed526eb1056f530121900aa9beb4627a2262589)) 228 | 229 | 230 | ### Documentation 231 | 232 | * ✏️ add docs info for remoteImagesDownloadsDelay config ([4978490](https://github.com/dc7290/next-export-optimize-images/commit/4978490d9c464af26cbd0539e5ec64e3f5d269d6)) 233 | 234 | ## [3.2.0](https://github.com/dc7290/next-export-optimize-images/compare/v3.1.1...v3.2.0) (2023-09-05) 235 | 236 | 237 | ### Features 238 | 239 | * 🚀 add Picture component ([0bf9352](https://github.com/dc7290/next-export-optimize-images/commit/0bf9352809f1d17aaf96231b33fee508930eea8b)), closes [#653](https://github.com/dc7290/next-export-optimize-images/issues/653) [#544](https://github.com/dc7290/next-export-optimize-images/issues/544) 240 | * 🚀 config generateFormats option ([fade51c](https://github.com/dc7290/next-export-optimize-images/commit/fade51cc0d22070984ba4cd3e49b933e17fed566)) 241 | 242 | 243 | ### Bug Fixes 244 | 245 | * 🐛 e2e test was not run ([446e208](https://github.com/dc7290/next-export-optimize-images/commit/446e208bc27855fa8b86bb8002d04e518f01824c)) 246 | * **deps:** update dependency sharp to ^0.32.5 ([07c4e9d](https://github.com/dc7290/next-export-optimize-images/commit/07c4e9d2a5b45bbb3846b15fabe1c1880d9c312e)) 247 | 248 | 249 | ### Documentation 250 | 251 | * ✏️ Picture component ([0ad3dc6](https://github.com/dc7290/next-export-optimize-images/commit/0ad3dc6a79e947a340deb061bbcdb31c6128efa8)) 252 | 253 | ## [3.1.1](https://github.com/dc7290/next-export-optimize-images/compare/v3.1.0...v3.1.1) (2023-08-31) 254 | 255 | 256 | ### Bug Fixes 257 | 258 | * 🐛 Align Svg handling with Next.js ([1d30298](https://github.com/dc7290/next-export-optimize-images/commit/1d30298ac6ccd925447bcaafb8605c74a39cd65d)) 259 | * 🐛 Fixed a problem with configurations not working ([92b544c](https://github.com/dc7290/next-export-optimize-images/commit/92b544cd7d251f109ff20f22ebff87417500eb1c)), closes [#612](https://github.com/dc7290/next-export-optimize-images/issues/612) 260 | * **deps:** update dependency sharp to ^0.32.3 ([b626e7f](https://github.com/dc7290/next-export-optimize-images/commit/b626e7f22181333bcebecf253301f93bfac2062c)) 261 | * **deps:** update dependency sharp to ^0.32.4 ([5217bdd](https://github.com/dc7290/next-export-optimize-images/commit/5217bdd7eb66deb9afc16971a48ef9234ebf80d5)) 262 | 263 | 264 | ### Documentation 265 | 266 | * Fix typo on structure page ([3ec2d6b](https://github.com/dc7290/next-export-optimize-images/commit/3ec2d6b5367896eac2d4e392f73241900cde11b3)) 267 | 268 | ## [3.1.0](https://github.com/dc7290/next-export-optimize-images/compare/v3.0.6...v3.1.0) (2023-07-14) 269 | 270 | 271 | ### Features 272 | 273 | * 🚀 Support functional remote-images ([87d71a8](https://github.com/dc7290/next-export-optimize-images/commit/87d71a868be15e49ddb7fa977302bdf3aca0c3a5)) 274 | 275 | 276 | ### Bug Fixes 277 | 278 | * 🐛 Fixed problem with settings not loading during build ([35c817e](https://github.com/dc7290/next-export-optimize-images/commit/35c817e4829f25c26bc7e4d315b9a2aa1e06b61f)), closes [#612](https://github.com/dc7290/next-export-optimize-images/issues/612) 279 | 280 | ## [3.0.6](https://github.com/dc7290/next-export-optimize-images/compare/v3.0.5...v3.0.6) (2023-07-04) 281 | 282 | 283 | ### Bug Fixes 284 | 285 | * 🐛 Supported Next.js13.4.8 ([5a0e7c6](https://github.com/dc7290/next-export-optimize-images/commit/5a0e7c651509956e35490a6376c2a7c127fd683e)) 286 | 287 | ## [3.0.5](https://github.com/dc7290/next-export-optimize-images/compare/v3.0.4...v3.0.5) (2023-07-01) 288 | 289 | 290 | ### Bug Fixes 291 | 292 | * 🐛 Fixed loader caching uselessly ([bc10e30](https://github.com/dc7290/next-export-optimize-images/commit/bc10e3077012869d801c3fff5e34148aeab0971a)) 293 | 294 | ## [3.0.4](https://github.com/dc7290/next-export-optimize-images/compare/v3.0.3...v3.0.4) (2023-07-01) 295 | 296 | 297 | ### Bug Fixes 298 | 299 | * 🐛 Corrected loader application conditions ([38b1b89](https://github.com/dc7290/next-export-optimize-images/commit/38b1b89fae357c0ccc62f31cd1d0f9272fbbba30)) 300 | 301 | ## [3.0.3](https://github.com/dc7290/next-export-optimize-images/compare/v3.0.2...v3.0.3) (2023-07-01) 302 | 303 | 304 | ### Bug Fixes 305 | 306 | * 🐛 Delete Warning for quality ([abe1f19](https://github.com/dc7290/next-export-optimize-images/commit/abe1f19f3378c6d2efcd013bdb3ad67780fe5ed5)) 307 | * 🐛 Fixed loader path ([e8bdc63](https://github.com/dc7290/next-export-optimize-images/commit/e8bdc6326889b858df599bde649d74edfebd9e14)) 308 | 309 | ## [3.0.2](https://github.com/dc7290/next-export-optimize-images/compare/v3.0.1...v3.0.2) (2023-07-01) 310 | 311 | 312 | ### Bug Fixes 313 | 314 | * 🐛 Fixed an export error ([696261d](https://github.com/dc7290/next-export-optimize-images/commit/696261d9b7d1e2dbb612236333a03b71af2ccd52)) 315 | 316 | ## [3.0.1](https://github.com/dc7290/next-export-optimize-images/compare/v3.0.0...v3.0.1) (2023-07-01) 317 | 318 | 319 | ### Bug Fixes 320 | 321 | * 🐛 Fixed support Next.js version ([4624986](https://github.com/dc7290/next-export-optimize-images/commit/4624986e4947a6f8a00980ab904b3f7581317cb7)) 322 | 323 | ## [3.0.0](https://github.com/dc7290/next-export-optimize-images/compare/v2.1.0...v3.0.0) (2023-07-01) 324 | 325 | 326 | ### ⚠ BREAKING CHANGES 327 | 328 | * 🧨Next.js version must be at least 13.3.2 329 | 330 | ### Features 331 | 332 | * 🚀 For external images, add them manually ([54a70cb](https://github.com/dc7290/next-export-optimize-images/commit/54a70cb67cb25de03aac39fc8ed8352fc512b989)) 333 | * 🚀 Supported AppRouter ([6e5fd71](https://github.com/dc7290/next-export-optimize-images/commit/6e5fd71d993fa188b29277170a7478920aa18301)), closes [#527](https://github.com/dc7290/next-export-optimize-images/issues/527) 334 | * 🚀 Supported next/dynamic ([857ae6b](https://github.com/dc7290/next-export-optimize-images/commit/857ae6b71ea948937ba8c85849e5dacc9380412a)), closes [#106](https://github.com/dc7290/next-export-optimize-images/issues/106) 335 | 336 | 337 | ### Bug Fixes 338 | 339 | * 🐛 Changed version of peerDependencies next to 13.0.0 ([fc6e534](https://github.com/dc7290/next-export-optimize-images/commit/fc6e534cf5c1bd770d13ace52c39e17258fe709e)) 340 | 341 | 342 | ### Documentation 343 | 344 | * ✏️ Fixed Docs ([50847a8](https://github.com/dc7290/next-export-optimize-images/commit/50847a81e6763fd2e4c8d9917f72d760435e2fbc)) 345 | 346 | ## [2.1.0](https://github.com/dc7290/next-export-optimize-images/compare/v2.0.1...v2.1.0) (2023-04-30) 347 | 348 | 349 | ### Features 350 | 351 | * 🚀 Support for animated images ([09fef4a](https://github.com/dc7290/next-export-optimize-images/commit/09fef4ab003a6c59287ff593915a5973a8772267)), closes [#495](https://github.com/dc7290/next-export-optimize-images/issues/495) 352 | 353 | 354 | ### Bug Fixes 355 | 356 | * **deps:** update dependency sharp to ^0.32.0 ([ba13935](https://github.com/dc7290/next-export-optimize-images/commit/ba139351d8010317db9ae22d56371cdc8718aa06)) 357 | * **deps:** update dependency sharp to ^0.32.1 ([07cf2a4](https://github.com/dc7290/next-export-optimize-images/commit/07cf2a4ce514ce4ceacd3863f0be8689c54a84c7)) 358 | 359 | ## [2.0.1](https://github.com/dc7290/next-export-optimize-images/compare/v2.0.0...v2.0.1) (2023-04-01) 360 | 361 | 362 | ### Bug Fixes 363 | 364 | * **deps:** update dependency cli-progress to ^3.12.0 ([7047e7a](https://github.com/dc7290/next-export-optimize-images/commit/7047e7ae4420981598ba5d123836903f0c951db3)) 365 | * **deps:** update dependency fs-extra to ^11.1.1 ([b16aed9](https://github.com/dc7290/next-export-optimize-images/commit/b16aed9ed27b6725b57f0ecb87a0a626b3a6394d)) 366 | * **deps:** update dependency sharp to ^0.31.3 ([7401a78](https://github.com/dc7290/next-export-optimize-images/commit/7401a78c48e044927e3992b548b10aecb141f6df)) 367 | 368 | 369 | ### Tests 370 | 371 | * 💍 Update snapshot-file for Jest29 ([6b2c8dc](https://github.com/dc7290/next-export-optimize-images/commit/6b2c8dc8b82e4f93a057b6239e341e845e8db0c5)) 372 | 373 | 374 | ### Documentation 375 | 376 | * ✏️ Update @heroicons/react to v2 ([9a78da2](https://github.com/dc7290/next-export-optimize-images/commit/9a78da2ca083ccd9bd2767412aead1ddc428ef6f)) 377 | 378 | ## [2.0.0](https://github.com/dc7290/next-export-optimize-images/compare/v1.9.2...v2.0.0) (2022-12-17) 379 | 380 | 381 | ### ⚠ BREAKING CHANGES 382 | 383 | * 🧨Next.js v12 and below have been dropped. 384 | * 🧨Node.js v14 has been dropped 385 | 386 | ### Features 387 | 388 | * 🚀 Node.js minimum version to 16 ([ac0d7f9](https://github.com/dc7290/next-export-optimize-images/commit/ac0d7f982f6250fc86dba88ae91d38ac00b4ce4d)) 389 | * 🚀 Support for Next.js v13 ([ff5a2e2](https://github.com/dc7290/next-export-optimize-images/commit/ff5a2e21fe17932668ca065e04fa3a7eae29c16e)) 390 | 391 | 392 | ### Bug Fixes 393 | 394 | * 🐛 Fix Config type ([7448857](https://github.com/dc7290/next-export-optimize-images/commit/7448857223a4f2b9da35b519b33b64a069ab446c)) 395 | * **deps:** update dependency fs-extra to v11 ([8285c8f](https://github.com/dc7290/next-export-optimize-images/commit/8285c8f9c2c3fbd2754cc773fe37643286a04e57)) 396 | 397 | 398 | ### Documentation 399 | 400 | * ✏️ Support for Next.js v13 ([51a0ac9](https://github.com/dc7290/next-export-optimize-images/commit/51a0ac96cad53f2678902610fd37c2de4ca76911)) 401 | 402 | ## [1.9.2](https://github.com/dc7290/next-export-optimize-images/compare/v1.9.1...v1.9.2) (2022-12-11) 403 | 404 | 405 | ### Bug Fixes 406 | 407 | * 🐛 Removed crypto dependency from client bundle ([ca4ea71](https://github.com/dc7290/next-export-optimize-images/commit/ca4ea71a28f43942971153c124cdac72799806d8)), closes [#332](https://github.com/dc7290/next-export-optimize-images/issues/332) 408 | * **deps:** update dependency got to ^11.8.6 ([3af53e8](https://github.com/dc7290/next-export-optimize-images/commit/3af53e8b342829cdc178f50ade120a374d0ad801)) 409 | 410 | ## [1.9.1](https://github.com/dc7290/next-export-optimize-images/compare/v1.9.0...v1.9.1) (2022-11-28) 411 | 412 | 413 | ### Bug Fixes 414 | 415 | * 🐛 ENAMETOOLONG when using long filenames ([eae8c74](https://github.com/dc7290/next-export-optimize-images/commit/eae8c74b1d0a29cfa66fdbcf55f5a0bb838bdea1)), closes [#309](https://github.com/dc7290/next-export-optimize-images/issues/309) 416 | * **deps:** update dependency sharp to ^0.31.1 ([eaa3506](https://github.com/dc7290/next-export-optimize-images/commit/eaa350636edda88022918df661385c80c531c966)) 417 | * **deps:** update dependency sharp to ^0.31.2 ([12f0a80](https://github.com/dc7290/next-export-optimize-images/commit/12f0a80efd85efdccef1866f0b8b554139fd2ebb)) 418 | 419 | ## [1.9.0](https://github.com/dc7290/next-export-optimize-images/compare/v1.8.0...v1.9.0) (2022-09-25) 420 | 421 | 422 | ### Features 423 | 424 | * 🚀 Support `next/future/image` ([7efc251](https://github.com/dc7290/next-export-optimize-images/commit/7efc251e0000493e4c5539983b79ed1eb0d840e6)), closes [#255](https://github.com/dc7290/next-export-optimize-images/issues/255) 425 | 426 | ## [1.8.0](https://github.com/dc7290/next-export-optimize-images/compare/v1.7.0...v1.8.0) (2022-09-23) 427 | 428 | 429 | ### Features 430 | 431 | * 🚀 Introduced new 'externalOutputDir' config flag ([e585af9](https://github.com/dc7290/next-export-optimize-images/commit/e585af9d115d5591bf3cdce1257f6be709dd9bb5)) 432 | 433 | 434 | ### Bug Fixes 435 | 436 | * 🐛 delete experimental ([b38cd9e](https://github.com/dc7290/next-export-optimize-images/commit/b38cd9e4cd3bf926ec1ebfacdc2e6cf808021276)) 437 | * 🐛 Remov unneeded leading slash from externalOutputDir flag ([e9de680](https://github.com/dc7290/next-export-optimize-images/commit/e9de680527156e10c63c9f4976737a3e074cc0f6)) 438 | * **deps:** update dependency sharp to ^0.31.0 ([ef3ed84](https://github.com/dc7290/next-export-optimize-images/commit/ef3ed84bb46925432cb6ed9c5c9c1bd185b841cf)) 439 | 440 | ## [1.7.0](https://github.com/dc7290/next-export-optimize-images/compare/v1.6.2...v1.7.0) (2022-08-31) 441 | 442 | 443 | ### Features 444 | 445 | * 🚀 Add sourceImageParser config option ([9b1fbd9](https://github.com/dc7290/next-export-optimize-images/commit/9b1fbd9ea698b927c2675cc3c6c99bb88135da9c)) 446 | 447 | 448 | ### Bug Fixes 449 | 450 | * 🐛 Documentation update ([21e1b8c](https://github.com/dc7290/next-export-optimize-images/commit/21e1b8c8e46ba768e70a7a89de0662c431660d81)) 451 | 452 | 453 | ### Continuous Integration 454 | 455 | * 🎡 Install `ts-node` ([2f3ef3b](https://github.com/dc7290/next-export-optimize-images/commit/2f3ef3ba68f72070badb7cc1c66980a97a23a7cf)) 456 | 457 | ## [1.6.2](https://github.com/dc7290/next-export-optimize-images/compare/v1.6.1...v1.6.2) (2022-08-27) 458 | 459 | 460 | ### Bug Fixes 461 | 462 | * 🐛 Processing when the image component is not used ([a3dd7b3](https://github.com/dc7290/next-export-optimize-images/commit/a3dd7b3cdae36620ccbbfea2a4c0f63261005720)), closes [#195](https://github.com/dc7290/next-export-optimize-images/issues/195) 463 | 464 | ## [1.6.1](https://github.com/dc7290/next-export-optimize-images/compare/v1.6.0...v1.6.1) (2022-08-24) 465 | 466 | 467 | ### Bug Fixes 468 | 469 | * **deps:** update dependency app-root-path to ^3.1.0 ([2593d23](https://github.com/dc7290/next-export-optimize-images/commit/2593d23f07366fb1cd40ffbdf9e4f32a6bee3d01)) 470 | * Minor grammar fix for a warning ([e439bc1](https://github.com/dc7290/next-export-optimize-images/commit/e439bc1fda9d7d73ee3848ea0efc1ce5ad656e36)) 471 | 472 | ## [1.6.0](https://github.com/dc7290/next-export-optimize-images/compare/v1.5.3...v1.6.0) (2022-07-30) 473 | 474 | 475 | ### Features 476 | 477 | * 🚀 Supprted monorepo ([13d5aff](https://github.com/dc7290/next-export-optimize-images/commit/13d5aff9d0130b859fb093fafd8545f2c0e4f4ec)), closes [#142](https://github.com/dc7290/next-export-optimize-images/issues/142) 478 | 479 | 480 | ### Bug Fixes 481 | 482 | * 🐛 Fix peer-dependence `next` version ([e71586c](https://github.com/dc7290/next-export-optimize-images/commit/e71586c89557061fb8a058ef4edcf86c11526e64)) 483 | 484 | ## [1.5.3](https://github.com/dc7290/next-export-optimize-images/compare/v1.5.2...v1.5.3) (2022-07-06) 485 | 486 | 487 | ### Bug Fixes 488 | 489 | * 🐛 Throws an error for `unoptimized` ([7e81b3b](https://github.com/dc7290/next-export-optimize-images/commit/7e81b3bbb511bb62431416bb8c710a223b59ceed)), closes [#129](https://github.com/dc7290/next-export-optimize-images/issues/129) 490 | * **deps:** update dependency cli-progress to ^3.11.2 ([f59d120](https://github.com/dc7290/next-export-optimize-images/commit/f59d120a25aff92f7d944637f62ba1025a00181f)) 491 | 492 | ## [1.5.2](https://github.com/dc7290/next-export-optimize-images/compare/v1.5.1...v1.5.2) (2022-06-29) 493 | 494 | 495 | ### Bug Fixes 496 | 497 | * **deps:** update dependency sharp to ^0.30.7 ([5dcb964](https://github.com/dc7290/next-export-optimize-images/commit/5dcb964a4d3ebc93052dce8619903e59860de256)) 498 | 499 | 500 | ### Documentation 501 | 502 | * ✏️ Applied the DocSearch ([01da0f9](https://github.com/dc7290/next-export-optimize-images/commit/01da0f97215449026511229d231a17640854a561)) 503 | 504 | 505 | ### Tests 506 | 507 | * 💍 Linting test files with recommended rules ([36d5343](https://github.com/dc7290/next-export-optimize-images/commit/36d53432a711cb18339152cb7600fb0e70fcac42)) 508 | 509 | ## [1.5.1](https://github.com/dc7290/next-export-optimize-images/compare/v1.5.0...v1.5.1) (2022-06-13) 510 | 511 | 512 | ### Bug Fixes 513 | 514 | * 🐛 Copy output of unsupported images ([7fefec5](https://github.com/dc7290/next-export-optimize-images/commit/7fefec5ba666a4673e87af4810408ab296c6fc38)), closes [#82](https://github.com/dc7290/next-export-optimize-images/issues/82) 515 | 516 | 517 | ### Documentation 518 | 519 | * ✏️ Added page about external images ([36422cc](https://github.com/dc7290/next-export-optimize-images/commit/36422ccbb99dc3b7cfed315c338798e28c399dad)) 520 | 521 | ## [1.5.0](https://github.com/dc7290/next-export-optimize-images/compare/v1.4.0...v1.5.0) (2022-06-12) 522 | 523 | 524 | ### Features 525 | 526 | * 🚀 About external-images by @dc7290 in https://github.com/dc7290/next-export-optimize-images/pull/97 527 | 528 | ## [1.4.0](https://github.com/dc7290/next-export-optimize-images/compare/v1.3.1...v1.4.0) (2022-06-07) 529 | 530 | 531 | ### Features 532 | 533 | * 🚀 Allow plugin to accept `configPath` ([864d02a](https://github.com/dc7290/next-export-optimize-images/commit/864d02ab6abe7fb6436b3cc871d6337a2d3ea6b3)) 534 | 535 | 536 | ### Bug Fixes 537 | 538 | * 🐛 Correctly parse path segment separators ([5b87790](https://github.com/dc7290/next-export-optimize-images/commit/5b87790d48438040ac307235ff7044b7b6f197eb)) 539 | * 🐛 Do not generate unnecessary placeholder images ([b8634da](https://github.com/dc7290/next-export-optimize-images/commit/b8634da4ce7f54bb49bab22f8614fa6115387f15)) 540 | * 🐛 Running mkdir in a windows environment ([37e5adf](https://github.com/dc7290/next-export-optimize-images/commit/37e5adfa19cb33c9c1402855d1b35b1d37b5e5ae)) 541 | * **deps:** update dependency sharp to ^0.30.6 ([1d97979](https://github.com/dc7290/next-export-optimize-images/commit/1d97979288aafb1949b6cfa04f9bfa71fd0eabe6)) 542 | 543 | 544 | ### Tests 545 | 546 | * 💍 Add e2e testing ([f601390](https://github.com/dc7290/next-export-optimize-images/commit/f6013903c6a22817de34f62c000e1f435db7f709)) 547 | * 💍 Fixed test in uniqueItems ([63390c4](https://github.com/dc7290/next-export-optimize-images/commit/63390c4c5737f51916b964896892b20612bf5a82)) 548 | 549 | 550 | ### Continuous Integration 551 | 552 | * 🎡 Allow windows to set env ([36c8788](https://github.com/dc7290/next-export-optimize-images/commit/36c87883b843984615103baf6ffe599b5327e82c)) 553 | * 🎡 Test different types of environments with matrix ([a598229](https://github.com/dc7290/next-export-optimize-images/commit/a5982298de26f1b049555c5d57c6d59a5ad9f845)) 554 | 555 | 556 | ### Documentation 557 | 558 | * ✏️ Add plugin-configuration page ([7548550](https://github.com/dc7290/next-export-optimize-images/commit/75485509e76f043c6a09be915259e451e5d96e88)) 559 | 560 | ## [1.3.1](https://github.com/dc7290/next-export-optimize-images/compare/v1.3.0...v1.3.1) (2022-05-29) 561 | 562 | 563 | ### Bug Fixes 564 | 565 | * **deps:** update dependency sharp to ^0.30.5 ([8e25077](https://github.com/dc7290/next-export-optimize-images/commit/8e250777cabea5e1022e7bbf6bcfc56c587144d6)) 566 | * **deps:** update docusaurus monorepo to v2.0.0-beta.21 ([74f635c](https://github.com/dc7290/next-export-optimize-images/commit/74f635c6dbbbac3ed5e6b471cca8571aea9fc598)) 567 | * **deps:** update react monorepo to v18 ([ea241dd](https://github.com/dc7290/next-export-optimize-images/commit/ea241dd293aa286892693a62ddb6b94970eae1aa)) 568 | 569 | 570 | ### Documentation 571 | 572 | * ✏️ Revise comparisons for clarity ([eb94a0e](https://github.com/dc7290/next-export-optimize-images/commit/eb94a0e9fe45915ae28a4116fdd2a8d8d54b658f)) 573 | 574 | ## [1.3.0](https://github.com/dc7290/next-export-optimize-images/compare/v1.2.0...v1.3.0) (2022-05-26) 575 | 576 | ### Features 577 | 578 | - ~~🚀 Add `convertFormat` ([423b819](https://github.com/dc7290/next-export-optimize-images/commit/423b819fef740fbf85757de5ee4bd6980c7754a6))~~ 579 | - ~~🚀 Measure the ASSETS in which errors occurred ([4833495](https://github.com/dc7290/next-export-optimize-images/commit/4833495fa91a5e839b1ae12b7464efe55180e08e))~~ 580 | 581 | ### Bug Fixes 582 | 583 | - 🐛 Correctly pass `basePath` to src and srcSet ([65916fe](https://github.com/dc7290/next-export-optimize-images/commit/65916fed15d0ea80dce88d775a1b2161717f676d)) 584 | 585 | ### Tests 586 | 587 | - ~~💍 Fixed config path ([6b6e986](https://github.com/dc7290/next-export-optimize-images/commit/6b6e986dac234eff9d48afaa1ff74ca3521e916a))~~ 588 | 589 | ### Continuous Integration 590 | 591 | - ~~🎡 Run `CI` in PR as well ([490bfd3](https://github.com/dc7290/next-export-optimize-images/commit/490bfd353d976068c28f4af765e2374dd32cae31))~~ 592 | 593 | ### Documentation 594 | 595 | - ~~✏️ Add `convertFormat` to the configuration page ([234a93f](https://github.com/dc7290/next-export-optimize-images/commit/234a93f9917145bf7cb038b936adf3cd08cf8e3f))~~ 596 | - ~~✏️ Add convertFormat to features ([a64c2cd](https://github.com/dc7290/next-export-optimize-images/commit/a64c2cd784bb6190d61b8ebc7f49d48be7674ab4))~~ 597 | - ~~✏️ Add description to Config ([5f21a62](https://github.com/dc7290/next-export-optimize-images/commit/5f21a62fa424857075e5b95203a28490978aceb6))~~ 598 | 599 | ## [1.2.0](https://github.com/dc7290/next-export-optimize-images/compare/v1.1.0...v1.2.0) (2022-05-25) 600 | 601 | ### Features 602 | 603 | - 🚀 Add `convertFormat` ([457c453](https://github.com/dc7290/next-export-optimize-images/commit/457c4530190c9bb3bc1001c3048a9b085ea65bc9)) 604 | - 🚀 Measure the ASSETS in which errors occurred ([d936772](https://github.com/dc7290/next-export-optimize-images/commit/d9367721507b31168000294c90027c4888ad0c7b)) 605 | 606 | ### Bug Fixes 607 | 608 | - **deps:** update dependency cli-progress to ^3.11.1 ([7f74945](https://github.com/dc7290/next-export-optimize-images/commit/7f7494563818cdcb9b90b1556039f04dab6f02c4)) 609 | - **deps:** update dependency prism-react-renderer to v1.3.3 ([7f8e3b2](https://github.com/dc7290/next-export-optimize-images/commit/7f8e3b21fd696340f1426015f6ae9dc84bef41db)) 610 | 611 | ### Continuous Integration 612 | 613 | - 🎡 Run `CI` in PR as well ([58f3077](https://github.com/dc7290/next-export-optimize-images/commit/58f307796819658dd6a7c6aa7c8d76a36cca84aa)) 614 | 615 | ### Tests 616 | 617 | - 💍 Fixed config path ([9ec7ae8](https://github.com/dc7290/next-export-optimize-images/commit/9ec7ae8e2d50745e6e149923edba48d0cc8db6e6)) 618 | 619 | ### Documentation 620 | 621 | - ✏️ Add `convertFormat` to the configuration page ([40f9b5a](https://github.com/dc7290/next-export-optimize-images/commit/40f9b5a11916840ce2ef0454e6be74f17fa675db)) 622 | - ✏️ Add description to Config ([4684cfc](https://github.com/dc7290/next-export-optimize-images/commit/4684cfc438b73f593cc18bebdac339cde48cfbe0)) 623 | - ✏️ Added `Some images are not displayed` to Q&A ([32e059a](https://github.com/dc7290/next-export-optimize-images/commit/32e059af0ed98f6f9ceeeca70242ac98b6949d59)) 624 | - ✏️ Set og:title ([50a92ab](https://github.com/dc7290/next-export-optimize-images/commit/50a92ab73952647256979a63e6ab903a973dd67c)) 625 | 626 | ## [1.1.0](https://github.com/dc7290/next-export-optimize-images/compare/v1.0.1...v1.1.0) (2022-05-21) 627 | 628 | ### Features 629 | 630 | - 🚀 Export type definitions withExportImages ([dfd1bdf](https://github.com/dc7290/next-export-optimize-images/commit/dfd1bdfa4d0b5afda0344721bde453adb3392ec8)) 631 | 632 | ### Performance Improvements 633 | 634 | - ⚡️ Use the option to prevent images from being enlarged ([a8209d7](https://github.com/dc7290/next-export-optimize-images/commit/a8209d7386645fc31e267add92dffc01d8884350)) 635 | 636 | ### Continuous Integration 637 | 638 | - 🎡 Change renovate schedule ([dd80a4d](https://github.com/dc7290/next-export-optimize-images/commit/dd80a4d961bfccff6c5e76914402cd305b2a17ed)) 639 | - 🎡 Corrected settings about releases ([7028fdd](https://github.com/dc7290/next-export-optimize-images/commit/7028fdd3d713e901e2eefcf8b56f8aed724cec21)) 640 | - 🎡 Register the title in @semantic-release/changelog ([9087554](https://github.com/dc7290/next-export-optimize-images/commit/908755407606d2775b0c9dde261cffe8c5088c38)) 641 | 642 | ### Documentation 643 | 644 | - ✏️ Add Comparison page ([9145d02](https://github.com/dc7290/next-export-optimize-images/commit/9145d02cd522d68ef5d6e61cd9d954af2fd5e4f6)) 645 | - ✏️ Add examples page ([2472113](https://github.com/dc7290/next-export-optimize-images/commit/2472113731b7faeffb26950e466afdc182b88c74)) 646 | - ✏️ Add footer link ([ed00e68](https://github.com/dc7290/next-export-optimize-images/commit/ed00e68d3b2eb0c79ee5926f7faa1b7abe691fc2)) 647 | - ✏️ Add local checks section in Getting Started ([6087661](https://github.com/dc7290/next-export-optimize-images/commit/60876611310d3bf78598a27d9c922d5ec97e95a9)) 648 | - ✏️ Add planned features page ([91b5bea](https://github.com/dc7290/next-export-optimize-images/commit/91b5bea44758b5d140c05524649d2cce7d4d5041)) 649 | - ✏️ Add Q&A page ([c8fbe69](https://github.com/dc7290/next-export-optimize-images/commit/c8fbe699fdac103eaec49071f7e679febeab9b34)) 650 | - ✏️ Add Structure page ([9e1f4a3](https://github.com/dc7290/next-export-optimize-images/commit/9e1f4a32f4a6d124db0b1d6b23048ea2a4a7a649)) 651 | - ✏️ Add text to introduction ([7af96e2](https://github.com/dc7290/next-export-optimize-images/commit/7af96e2191c36f8c97dec090b3b77f9f25dffab6)) 652 | - ✏️ Add title to code block ([45052ce](https://github.com/dc7290/next-export-optimize-images/commit/45052ce6ad719dfa0390eb8bb3f5d3f7606374df)) 653 | - ✏️ Added description ([eeef6a8](https://github.com/dc7290/next-export-optimize-images/commit/eeef6a8979aa9caad415586f5ffc9e7b3b1cb2ec)) 654 | - ✏️ Change link text to Introduction ([cd7b635](https://github.com/dc7290/next-export-optimize-images/commit/cd7b635578d65aa8e220a8879116e43296a79748)) 655 | - ✏️ Create Getting Started as a separate page ([9d37e87](https://github.com/dc7290/next-export-optimize-images/commit/9d37e872f47e09306111bb849216d45e421f34e0)) 656 | - ✏️ Highlight differences ([d2a76bd](https://github.com/dc7290/next-export-optimize-images/commit/d2a76bddd448477fba6d1d244d9863a3db99ce7f)) 657 | - ✏️ README Change home page to document site ([422f8db](https://github.com/dc7290/next-export-optimize-images/commit/422f8db2ae08e396ecd5b3987c264f645f9ca674)) 658 | 659 | ## [1.0.1](https://github.com/dc7290/next-export-optimize-images/compare/v1.0.0...v1.0.1) (2022-05-19) 660 | 661 | ### Bug Fixes 662 | 663 | - **deps:** update dependency ansi-colors to ^4.1.3 ([82f2027](https://github.com/dc7290/next-export-optimize-images/commit/82f20276eabc35b5dbe83d54d0dcf0b12d553ae1)) 664 | 665 | ### Documentation 666 | 667 | - ✏️ add logo and ogp ([ef3197b](https://github.com/dc7290/next-export-optimize-images/commit/ef3197b0a6304153c1681288aa5d6941bfcd1ce0)) 668 | - ✏️ Added `Configuration` page ([365fbd4](https://github.com/dc7290/next-export-optimize-images/commit/365fbd45777a0d22de6aefe3b7eb1ad68da0f614)) 669 | - ✏️ Added top page ([2814451](https://github.com/dc7290/next-export-optimize-images/commit/2814451edd9f8b695f028174091ab381785a984c)) 670 | - ✏️ Configuration→ Usage ([06994f1](https://github.com/dc7290/next-export-optimize-images/commit/06994f17009e679d93c0829722c8efb753f8eb27)) 671 | - ✏️ Corrected title ([d2a8d89](https://github.com/dc7290/next-export-optimize-images/commit/d2a8d8939d0e6b6efbd1fdb05e590bc0fe403b3f)) 672 | - ✏️ Feature を修正 ([ea7ef5f](https://github.com/dc7290/next-export-optimize-images/commit/ea7ef5f82cd5f2af8281729dcd3e0211855b98b1)) 673 | - ✏️ Fixed README ([f58bf08](https://github.com/dc7290/next-export-optimize-images/commit/f58bf0805328fc2df100862abcb746ce4314a4df)) 674 | - ✏️ README の Feature を修正 ([3c02b52](https://github.com/dc7290/next-export-optimize-images/commit/3c02b52c64e611a4668865b52a7b1b8f160b3b2b)) 675 | - ✏️ Top page modification ([533064b](https://github.com/dc7290/next-export-optimize-images/commit/533064b558140dc8709aa39297af8fc0de0752cf)) 676 | 677 | ### Continuous Integration 678 | 679 | - 🎡 @semantic-release/exec をプラグインに追加 ([8ca1957](https://github.com/dc7290/next-export-optimize-images/commit/8ca19574f5c585bf8b07f93d8be86913feceb6c0)) 680 | - 🎡 Added faketty ([a01d7fa](https://github.com/dc7290/next-export-optimize-images/commit/a01d7fa36e9c9d1889c4a9cffd882cee5ed6b8e8)) 681 | - 🎡 Github Actions: Release does not run git-cz ([14d25f3](https://github.com/dc7290/next-export-optimize-images/commit/14d25f3520c1079bbb8adb4c561ac8ffd5f5c48c)) 682 | - 🎡 リリースブランチを release に変更 ([30b02d5](https://github.com/dc7290/next-export-optimize-images/commit/30b02d5e55a5418ee9befebbff2c72ae03f27c07)) 683 | - 🎡 名前の変更 ([f32209a](https://github.com/dc7290/next-export-optimize-images/commit/f32209acf0844e65c9ca11dbd1d7faa6fd7d049a)) 684 | 685 | ## [1.0.0](https://github.com/dc7290/next-export-optimize-images/releases/tag/v1.0.0) (2022-05-17) 686 | 687 | ### Added 688 | 689 | - Everything! 690 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Your contributions are most welcome! 4 | These guidelines will be useful for Issues and PR. 5 | 6 | ## Issue 7 | 8 | Basically, please report using the template provided. 9 | Of course, you may also write your own report. 10 | 11 | The template contains a 12 | 13 | - Bug report 14 | - Feature request 15 | 16 | and choose the one that best suits the issue you wish to submit. 17 | 18 | Also, please help us by filling out the form as much as possible, as it will speed up the time it takes to fix bugs and add features. 19 | 20 | ## Pull Request 21 | 22 | Fork this repository and create a branch from the latest `main` branch. 23 | 24 | ### Setup 25 | 26 | Run `npm i` to install dependencies. 27 | 28 | ### Commit 29 | 30 | When committing, changed files are automatically linted and formatted, and commit messages are created interactively by `git-cz`. 31 | (Attention: `git commit` will automatically run `git-cz`.) 32 | 33 | The rules for doing so are as follows. 34 | 35 | | type | description | 36 | | -------- | -------------------------------------------------------- | 37 | | feat | A new feature | 38 | | fix | A bug fix | 39 | | sec | A vulnerability fix | 40 | | perf | A code change that improves performance | 41 | | refactor | A code change that neither fixes a bug or adds a feature | 42 | | docs | Documentation only changes | 43 | | release | Create a release commit | 44 | | style | Markup, white-space, formatting, missing semi-colons... | 45 | | test | Adding missing tests | 46 | | ci | CI related changes | 47 | | chore | Build process or auxiliary tool changes | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 d-suke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | npm 5 | downloads 6 | License 7 | Node.js CI 8 | GitHub Repo stars 9 |
10 | 11 | # Next Export Optimize Images 12 | 13 | Using this repository, you can get the full benefits of `next/image` even when using `next export` by doing image optimization at build time. 14 | 15 | This makes it possible to build a high performance website with this solution, whether you want to build a simple website or a completely static output. 16 | 17 | ## Feature 18 | 19 | - Optimize images at build time. 20 | - All options for `next/image` available 21 | - Convert formats (png → webp, etc.) 22 | - Download external images locally. 23 | - Using `sharp`, so it's fast. 24 | - Cache prevents repeating the same optimization 25 | - Support TypeScript 26 | - Support AppRouter 27 | 28 | ## Installation 29 | 30 | ```bash 31 | npm install next-export-optimize-images 32 | ``` 33 | 34 | ## Document Site 35 | 36 | https://next-export-optimize-images.vercel.app 37 | 38 | ### DeepWiki 39 | 40 | https://deepwiki.com/dc7290/next-export-optimize-images 41 | 42 | ## License 43 | 44 | Next Export Optimize Images is available under the MIT License. 45 | -------------------------------------------------------------------------------- /__tests__/cli/cache/fixtures/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/cli/cache/fixtures/default.png -------------------------------------------------------------------------------- /__tests__/cli/cache/index.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | import fs from 'fs-extra' 4 | 5 | import { getOptimizeResult } from '../../../src/cli' 6 | import type { CacheImages } from '../../../src/cli/utils/cache' 7 | 8 | const cacheDir = path.resolve(__dirname, '.cache') 9 | 10 | jest.setTimeout(60 * 3 * 1000) 11 | 12 | beforeAll( 13 | async () => { 14 | const resultsDir = path.resolve(__dirname, 'results') 15 | 16 | await Promise.all([fs.remove(resultsDir), fs.remove(cacheDir)]) 17 | await fs.mkdirp(path.join(resultsDir, '_next/static/chunks/images')) 18 | }, 19 | 60 * 3 * 1000 20 | ) 21 | 22 | describe('Cache', () => { 23 | test('Cache is created and optimization is skipped', async () => { 24 | const manifest = [ 25 | { 26 | output: '/_next/static/chunks/images/default_1920_75.png', 27 | src: '/default.png', 28 | width: 1920, 29 | quality: 75, 30 | extension: 'png', 31 | }, 32 | { 33 | output: '/_next/static/chunks/images/default_3840_75.png', 34 | src: '/default.png', 35 | width: 3840, 36 | quality: 75, 37 | extension: 'png', 38 | }, 39 | ] 40 | 41 | let measuredCache = 0 42 | let measuredNonCache = 0 43 | let measuredError = 0 44 | 45 | const destDir = path.resolve(__dirname, 'results') 46 | const cacheMeasurement = () => { 47 | measuredCache += 1 48 | } 49 | const nonCacheMeasurement = () => { 50 | measuredNonCache += 1 51 | } 52 | const errorMeasurement = () => { 53 | measuredError += 1 54 | } 55 | const cliProgressBarIncrement = () => undefined 56 | const srcDir = path.resolve(__dirname, 'fixtures') 57 | 58 | const cacheImages: CacheImages = [] 59 | 60 | await Promise.all( 61 | manifest.map(async (item) => { 62 | const imageBuffer = await fs.readFile(path.join(srcDir, item.src)) 63 | 64 | return getOptimizeResult({ 65 | imageBuffer, 66 | destDir, 67 | noCache: false, 68 | cacheImages, 69 | cacheDir, 70 | cacheMeasurement, 71 | nonCacheMeasurement, 72 | errorMeasurement, 73 | pushInvalidFormatAssets: () => undefined, 74 | cliProgressBarIncrement, 75 | originalFilePath: path.join(srcDir, item.src), 76 | ...item, 77 | }) 78 | }) 79 | ) 80 | 81 | await Promise.all( 82 | manifest.map(async (item) => { 83 | const imageBuffer = await fs.readFile(path.join(srcDir, item.src)) 84 | 85 | return getOptimizeResult({ 86 | imageBuffer, 87 | destDir, 88 | noCache: false, 89 | cacheImages, 90 | cacheDir, 91 | cacheMeasurement, 92 | nonCacheMeasurement, 93 | errorMeasurement, 94 | pushInvalidFormatAssets: () => undefined, 95 | cliProgressBarIncrement, 96 | originalFilePath: path.join(srcDir, item.src), 97 | ...item, 98 | }) 99 | }) 100 | ) 101 | 102 | expect(measuredCache).toBe(2) 103 | expect(measuredNonCache).toBe(2) 104 | expect(measuredError).toBe(0) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /__tests__/cli/external-images/index.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | import fs from 'fs-extra' 4 | import { imageConfigDefault } from 'next/dist/shared/lib/image-config' 5 | 6 | import { optimizeImages } from '../../../src/cli' 7 | 8 | const fixturesDir = path.resolve(__dirname, 'fixtures') 9 | 10 | beforeAll( 11 | async () => { 12 | await fs.remove(fixturesDir) 13 | await optimizeImages({ 14 | manifestJsonPath: path.resolve(__dirname, 'manifest.json'), 15 | noCache: true, 16 | terse: true, 17 | config: { 18 | outDir: '__tests__/cli/external-images/fixtures', 19 | }, 20 | nextImageConfig: imageConfigDefault, 21 | }) 22 | }, 23 | 60 * 3 * 1000 24 | ) 25 | 26 | const exist = (filename: string) => fs.existsSync(path.join(fixturesDir, '_next/static/media', filename)) 27 | 28 | describe('External Image Optimization.', () => { 29 | test('Downloadable', () => { 30 | expect(exist('og.png')).toBeTruthy() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /__tests__/cli/external-images/manifest.json: -------------------------------------------------------------------------------- 1 | {"output":"/_next/static/chunks/images/og_1920_75.webp","src":"/_next/static/media/og.png","width":1920,"quality":75,"extension":"webp","externalUrl":"https://next-export-optimize-images.vercel.app/og.png"} 2 | {"output":"/_next/static/chunks/images/og_3840_75.webp","src":"/_next/static/media/og.png","width":3840,"quality":75,"extension":"webp","externalUrl":"https://next-export-optimize-images.vercel.app/og.png"} -------------------------------------------------------------------------------- /__tests__/cli/image-optimize/fixtures/default.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/cli/image-optimize/fixtures/default.avif -------------------------------------------------------------------------------- /__tests__/cli/image-optimize/fixtures/default.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/cli/image-optimize/fixtures/default.jpeg -------------------------------------------------------------------------------- /__tests__/cli/image-optimize/fixtures/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/cli/image-optimize/fixtures/default.jpg -------------------------------------------------------------------------------- /__tests__/cli/image-optimize/fixtures/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/cli/image-optimize/fixtures/default.png -------------------------------------------------------------------------------- /__tests__/cli/image-optimize/fixtures/default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /__tests__/cli/image-optimize/fixtures/default.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/cli/image-optimize/fixtures/default.webp -------------------------------------------------------------------------------- /__tests__/cli/image-optimize/index.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | import fs from 'fs-extra' 4 | import { imageConfigDefault } from 'next/dist/shared/lib/image-config' 5 | 6 | import { optimizeImages } from '../../../src/cli' 7 | 8 | beforeAll( 9 | async () => { 10 | await optimizeImages({ 11 | manifestJsonPath: path.resolve(__dirname, 'manifest.json'), 12 | noCache: true, 13 | terse: true, 14 | config: { 15 | outDir: '__tests__/cli/image-optimize/fixtures', 16 | }, 17 | nextImageConfig: imageConfigDefault, 18 | }) 19 | }, 20 | 60 * 3 * 1000 21 | ) 22 | 23 | const exist = (filename: string) => fs.existsSync(path.resolve(__dirname, 'fixtures/results/images', filename)) 24 | 25 | describe('Image optimization.', () => { 26 | test('png images optimized', () => { 27 | expect(exist('default_10_75.png')).toBeTruthy() 28 | expect(exist('default_1920_75.png')).toBeTruthy() 29 | expect(exist('default_3840_75.png')).toBeTruthy() 30 | }) 31 | 32 | test('jpeg images optimized', () => { 33 | expect(exist('default_10_75.jpeg')).toBeTruthy() 34 | expect(exist('default_1920_75.jpeg')).toBeTruthy() 35 | expect(exist('default_3840_75.jpeg')).toBeTruthy() 36 | }) 37 | 38 | test('jpg images optimized', () => { 39 | expect(exist('default_10_75.jpg')).toBeTruthy() 40 | expect(exist('default_1920_75.jpg')).toBeTruthy() 41 | expect(exist('default_3840_75.jpg')).toBeTruthy() 42 | }) 43 | 44 | test('webp images optimized', () => { 45 | expect(exist('default_10_75.webp')).toBeTruthy() 46 | expect(exist('default_1920_75.webp')).toBeTruthy() 47 | expect(exist('default_3840_75.webp')).toBeTruthy() 48 | }) 49 | 50 | test('avif images optimized', () => { 51 | expect(exist('default_10_75.avif')).toBeTruthy() 52 | expect(exist('default_1920_75.avif')).toBeTruthy() 53 | expect(exist('default_3840_75.avif')).toBeTruthy() 54 | }) 55 | 56 | test('svg images copied', () => { 57 | expect(exist('default_10_75.svg')).toBeTruthy() 58 | expect(exist('default_1920_75.svg')).toBeTruthy() 59 | expect(exist('default_3840_75.svg')).toBeTruthy() 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /__tests__/cli/image-optimize/manifest.json: -------------------------------------------------------------------------------- 1 | {"output":"/results/images/default_10_75.png","src":"/default.png","width":10,"quality":75,"extension":"png"} 2 | {"output":"/results/images/default_1920_75.png","src":"/default.png","width":1920,"quality":75,"extension":"png"} 3 | {"output":"/results/images/default_3840_75.png","src":"/default.png","width":3840,"quality":75,"extension":"png"} 4 | {"output":"/results/images/default_10_75.jpeg","src":"/default.jpeg","width":10,"quality":75,"extension":"jpeg"} 5 | {"output":"/results/images/default_1920_75.jpeg","src":"/default.jpeg","width":1920,"quality":75,"extension":"jpeg"} 6 | {"output":"/results/images/default_3840_75.jpeg","src":"/default.jpeg","width":3840,"quality":75,"extension":"jpeg"} 7 | {"output":"/results/images/default_10_75.jpg","src":"/default.jpg","width":10,"quality":75,"extension":"jpg"} 8 | {"output":"/results/images/default_1920_75.jpg","src":"/default.jpg","width":1920,"quality":75,"extension":"jpg"} 9 | {"output":"/results/images/default_3840_75.jpg","src":"/default.jpg","width":3840,"quality":75,"extension":"jpg"} 10 | {"output":"/results/images/default_10_75.webp","src":"/default.webp","width":10,"quality":75,"extension":"webp"} 11 | {"output":"/results/images/default_1920_75.webp","src":"/default.webp","width":1920,"quality":75,"extension":"webp"} 12 | {"output":"/results/images/default_3840_75.webp","src":"/default.webp","width":3840,"quality":75,"extension":"webp"} 13 | {"output":"/results/images/default_10_75.avif","src":"/default.avif","width":10,"quality":75,"extension":"avif"} 14 | {"output":"/results/images/default_1920_75.avif","src":"/default.avif","width":1920,"quality":75,"extension":"avif"} 15 | {"output":"/results/images/default_3840_75.avif","src":"/default.avif","width":3840,"quality":75,"extension":"avif"} 16 | {"output":"/results/images/default_10_75.svg","src":"/default.svg","width":10,"quality":75,"extension":"svg"} 17 | {"output":"/results/images/default_1920_75.svg","src":"/default.svg","width":1920,"quality":75,"extension":"svg"} 18 | {"output":"/results/images/default_3840_75.svg","src":"/default.svg","width":3840,"quality":75,"extension":"svg"} 19 | -------------------------------------------------------------------------------- /__tests__/cli/uniqby/index.test.ts: -------------------------------------------------------------------------------- 1 | import uniqueItems from '../../../src/cli/utils/uniqueItems' 2 | 3 | const items = [ 4 | { 5 | output: '/_next/static/chunks/images/default_1920_75.png', 6 | src: '/default.png', 7 | width: 1920, 8 | quality: 75, 9 | extension: 'png', 10 | }, 11 | { 12 | output: '/_next/static/chunks/images/default_1920_75.png', 13 | src: '/default.png', 14 | width: 1920, 15 | quality: 75, 16 | extension: 'png', 17 | }, 18 | { 19 | output: '/_next/static/chunks/images/default_1920_75.png', 20 | src: '/default.png', 21 | width: 1920, 22 | quality: 75, 23 | extension: 'png', 24 | }, 25 | { 26 | output: '/_next/static/chunks/images/default_1920_75.png', 27 | src: '/default.png', 28 | width: 1920, 29 | quality: 75, 30 | extension: 'png', 31 | }, 32 | { 33 | output: '/_next/static/chunks/images/default_3840_75.png', 34 | src: '/default.png', 35 | width: 3840, 36 | quality: 75, 37 | extension: 'png', 38 | }, 39 | ] 40 | 41 | describe('uniqby', () => { 42 | test('Duplicate items are eliminated.', () => { 43 | expect(uniqueItems(items)).toEqual([ 44 | { 45 | extension: 'png', 46 | output: '/_next/static/chunks/images/default_1920_75.png', 47 | quality: 75, 48 | src: '/default.png', 49 | width: 1920, 50 | }, 51 | { 52 | output: '/_next/static/chunks/images/default_3840_75.png', 53 | src: '/default.png', 54 | width: 3840, 55 | quality: 75, 56 | extension: 'png', 57 | }, 58 | ]) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /__tests__/e2e-build/app/layout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function RootLayout({ children }) { 4 | return ( 5 | 6 | {children} 7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /__tests__/e2e-build/app/page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Image from '../../../image' 3 | 4 | import imgSrc from '../src/images/img.png' 5 | 6 | export default function IndexPage() { 7 | return ( 8 |
9 | {/* Imported image */} 10 | 11 | 12 | {/* Static image */} 13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /__tests__/e2e-build/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../../dist/cli').run({ 3 | noCache: true, 4 | }) 5 | -------------------------------------------------------------------------------- /__tests__/e2e-build/export-images.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('../../src').Config} 3 | */ 4 | const config = { 5 | sharpOptions: { 6 | webp: { 7 | effort: 0, 8 | }, 9 | }, 10 | remoteImages: async () => ['https://picsum.photos/id/237/200/300.jpg', 'https://picsum.photos/id/238/200/300.jpg'], 11 | mode: 'build', 12 | } 13 | 14 | module.exports = config 15 | -------------------------------------------------------------------------------- /__tests__/e2e-build/index.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | import fs from 'fs-extra' 4 | import { imageConfigDefault } from 'next/dist/shared/lib/image-config' 5 | 6 | const exist = (filename: string) => fs.existsSync(path.resolve(__dirname, '.next/static/chunks/images', filename)) 7 | 8 | const files = [ 9 | // webp 10 | 11 | // next/image 12 | '_next/static/media/img.8a5ad2fe_[width].webp', 13 | 'id/237/200/300_[width].webp', 14 | 'id/238/200/300_[width].webp', 15 | 16 | // png or jpg 17 | 18 | // next/image 19 | '_next/static/media/img.8a5ad2fe_[width].png', 20 | 'id/237/200/300_[width].jpg', 21 | 'id/238/200/300_[width].jpg', 22 | ] 23 | 24 | describe('`next build && next export && next-export-optimize-images` is executed correctly', () => { 25 | test('Images are being generated.', async () => { 26 | const customConfig = require('./next.config.js') 27 | const configImages = { ...imageConfigDefault, ...customConfig.images } 28 | const allSizes = [...configImages.imageSizes, ...configImages.deviceSizes] 29 | for (const size of allSizes) { 30 | for (const file of files) { 31 | const isExist = exist(file.replace('[width]', size.toString())) 32 | if (!isExist) { 33 | console.log(file.replace('[width]', size.toString())) 34 | } 35 | expect(isExist).toBeTruthy() 36 | } 37 | } 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /__tests__/e2e-build/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /__tests__/e2e-build/next.config.js: -------------------------------------------------------------------------------- 1 | const withExportImages = require('../../dist') 2 | 3 | /** 4 | * @type {import('next').NextConfig} 5 | */ 6 | const config = { 7 | reactStrictMode: true, 8 | eslint: { 9 | ignoreDuringBuilds: true, 10 | }, 11 | } 12 | 13 | module.exports = withExportImages(config, { __test: true }) 14 | -------------------------------------------------------------------------------- /__tests__/e2e-build/public/images/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e-build/public/images/img.png -------------------------------------------------------------------------------- /__tests__/e2e-build/src/images/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e-build/src/images/img.png -------------------------------------------------------------------------------- /__tests__/e2e/app/layout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function RootLayout({ children }) { 4 | return ( 5 | 6 | {children} 7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /__tests__/e2e/app/page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Image from '../../../image' 4 | import LegacyImage from '../../../legacy/image' 5 | import Picture from '../../../picture' 6 | import RemoteImage from '../../../remote-image' 7 | import RemotePicture from '../../../remote-picture' 8 | import ClientComponent from '../components/ClientComponent' 9 | import WithPropsComponent from '../components/WithPropsComponent' 10 | import imgSrc from '../images/img.png' 11 | import legacyImgSrc from '../images/legacy-img.png' 12 | import pictureSrc from '../images/picture.png' 13 | 14 | const id = 400 15 | 16 | export default function IndexPage() { 17 | return ( 18 | <> 19 | {/* next/image */} 20 |
21 | {/* Imported image */} 22 | 23 | 24 | {/* Static image */} 25 | 26 | 27 | {/* Image with props */} 28 | 29 | 30 | {/* Invalid format image */} 31 | 32 | 33 | {/* External Image with RemoteImage */} 34 | 35 | 36 | {/* External Image with RemoteImage dynamic src */} 37 | 38 | 39 | {/* External Image with RemotePicture */} 40 | 41 | 42 | {/* Animated image */} 43 | 44 | 45 | 46 |
47 | {/* picture */} 48 |
49 | {/* Imported image */} 50 | 51 | 52 | {/* Static image */} 53 | 54 |
55 | {/* next/legacy/image */} 56 |
57 | {/* Imported image */} 58 | 59 | 60 | {/* Static image */} 61 | 68 |
69 | 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /__tests__/e2e/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../../dist/cli').run({ 3 | noCache: true, 4 | }) 5 | -------------------------------------------------------------------------------- /__tests__/e2e/components/ClientComponent.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useState, useEffect } from 'react' 4 | import Image from '../../../image' 5 | 6 | import clientOnlySrc from '../images/client-only.png' 7 | 8 | const ClientComponent = () => { 9 | const [isClient, setIsClient] = useState(false) 10 | useEffect(() => { 11 | setIsClient(true) 12 | }, []) 13 | 14 | return isClient ? : null 15 | } 16 | 17 | export default ClientComponent 18 | -------------------------------------------------------------------------------- /__tests__/e2e/components/WithPropsComponent.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import { getOptimizedImageProps } from '../../../image' 5 | 6 | import getPropsSrc from '../images/get-props.png' 7 | import getPropsMobileSrc from '../images/get-props-mobile.png' 8 | 9 | const WithPropsComponent = () => { 10 | const props = getOptimizedImageProps({ src: getPropsSrc, alt: '' }).props 11 | const mobileProps = getOptimizedImageProps({ src: getPropsMobileSrc, alt: '' }).props 12 | 13 | return ( 14 |
15 | 16 |
23 | 24 | 30 | 31 | 32 |
33 | ) 34 | } 35 | 36 | export default WithPropsComponent 37 | -------------------------------------------------------------------------------- /__tests__/e2e/export-images.config.js: -------------------------------------------------------------------------------- 1 | const getRemoteImages = () => 2 | new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(['https://picsum.photos/id/237/200/300.jpg', 'https://picsum.photos/id/238/200/300.jpg']) 5 | }, 500) 6 | }) 7 | 8 | /** 9 | * @type {import('../../src').Config} 10 | */ 11 | const config = { 12 | sharpOptions: { 13 | webp: { 14 | effort: 0, 15 | }, 16 | }, 17 | generateFormats: ['avif', 'webp'], 18 | remoteImages: getRemoteImages, 19 | filenameGenerator: ({ path, name, width, extension }) => `${path}/${name}_${width}.${extension}`, 20 | // function宣言による記述もテストしたいため無視する 21 | // biome-ignore lint/complexity/useArrowFunction: 22 | sourceImageParser: function ({ src, defaultParser }) { 23 | return defaultParser(src) 24 | }, 25 | cacheDir: '.next/cache/next-export-optimize-images', 26 | ignorePaths: ['images/ignore-img.png'], 27 | } 28 | 29 | module.exports = config 30 | -------------------------------------------------------------------------------- /__tests__/e2e/images/client-only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e/images/client-only.png -------------------------------------------------------------------------------- /__tests__/e2e/images/get-props-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e/images/get-props-mobile.png -------------------------------------------------------------------------------- /__tests__/e2e/images/get-props.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e/images/get-props.png -------------------------------------------------------------------------------- /__tests__/e2e/images/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e/images/img.png -------------------------------------------------------------------------------- /__tests__/e2e/images/legacy-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e/images/legacy-img.png -------------------------------------------------------------------------------- /__tests__/e2e/images/picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e/images/picture.png -------------------------------------------------------------------------------- /__tests__/e2e/index.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | import fs from 'fs-extra' 4 | import { imageConfigDefault } from 'next/dist/shared/lib/image-config' 5 | 6 | const exist = (filename: string) => fs.existsSync(path.resolve(__dirname, 'out/_next/static/chunks/images', filename)) 7 | 8 | const files = [ 9 | // avif 10 | 11 | // next/image 12 | '_next/static/media/img.8a5ad2fe_[width].avif', 13 | '_next/static/media/get-props.8a5ad2fe_[width].avif', 14 | '_next/static/media/get-props-mobile.7d5f5264_[width].avif', 15 | 'images/img_[width].avif', 16 | 'id/237/200/300_[width].avif', 17 | 'id/238/200/300_[width].avif', 18 | 'id/500/200/400_[width].avif', 19 | 'images/animated_[width].avif', 20 | '_next/static/media/client-only.8a5ad2fe_[width].avif', 21 | // next/legacy/image 22 | '_next/static/media/legacy-img.8a5ad2fe_[width].avif', 23 | 'images/legacy-img_[width].avif', 24 | // picture 25 | '_next/static/media/picture.8a5ad2fe_[width].avif', 26 | 'images/picture_[width].avif', 27 | 28 | // webp 29 | 30 | // next/image 31 | '_next/static/media/img.8a5ad2fe_[width].webp', 32 | '_next/static/media/get-props.8a5ad2fe_[width].webp', 33 | '_next/static/media/get-props-mobile.7d5f5264_[width].webp', 34 | 'images/img_[width].webp', 35 | 'id/237/200/300_[width].webp', 36 | 'id/238/200/300_[width].webp', 37 | 'id/500/200/400_[width].webp', 38 | 'images/animated_[width].webp', 39 | '_next/static/media/client-only.8a5ad2fe_[width].webp', 40 | // next/legacy/image 41 | '_next/static/media/legacy-img.8a5ad2fe_[width].webp', 42 | 'images/legacy-img_[width].webp', 43 | // picture 44 | '_next/static/media/picture.8a5ad2fe_[width].webp', 45 | 'images/picture_[width].webp', 46 | 47 | // png or jpg 48 | 49 | // next/image 50 | '_next/static/media/img.8a5ad2fe_[width].png', 51 | '_next/static/media/get-props.8a5ad2fe_[width].png', 52 | '_next/static/media/get-props-mobile.7d5f5264_[width].png', 53 | 'images/img_[width].png', 54 | 'id/237/200/300_[width].jpg', 55 | 'id/238/200/300_[width].jpg', 56 | 'id/300/200/400_[width].jpg', 57 | 'id/400/200/400_[width].jpg', 58 | 'id/500/200/400_[width].jpg', 59 | '_next/static/media/client-only.8a5ad2fe_[width].png', 60 | // next/legacy/image 61 | '_next/static/media/legacy-img.8a5ad2fe_[width].png', 62 | 'images/legacy-img_[width].png', 63 | // picture 64 | '_next/static/media/picture.8a5ad2fe_[width].png', 65 | 'images/picture_[width].png', 66 | ] 67 | 68 | describe('`next build && next export && next-export-optimize-images` is executed correctly', () => { 69 | test('Images are being generated.', async () => { 70 | const customConfig = await require('./next.config.js') 71 | const configImages = { ...imageConfigDefault, ...customConfig.images } 72 | const allSizes = [...configImages.imageSizes, ...configImages.deviceSizes] 73 | for (const size of allSizes) { 74 | for (const file of files) { 75 | const isExist = exist(file.replace('[width]', size.toString())) 76 | if (!isExist) { 77 | console.log(file.replace('[width]', size.toString())) 78 | } 79 | expect(isExist).toBeTruthy() 80 | } 81 | } 82 | }) 83 | 84 | test('ignorePaths is working.', async () => { 85 | const isExist = exist('images/ignore-img.png') 86 | expect(isExist).toBeFalsy() 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /__tests__/e2e/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /__tests__/e2e/next.config.js: -------------------------------------------------------------------------------- 1 | const withExportImages = require('../../dist') 2 | 3 | /** 4 | * @type {import('next').NextConfig} 5 | */ 6 | const config = { 7 | reactStrictMode: true, 8 | eslint: { 9 | ignoreDuringBuilds: true, 10 | }, 11 | output: 'export', 12 | images: { 13 | deviceSizes: [320, 480, 768, 1024, 1440, 1920], 14 | }, 15 | } 16 | 17 | module.exports = withExportImages(config, { __test: true }) 18 | -------------------------------------------------------------------------------- /__tests__/e2e/public/images/animated.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e/public/images/animated.webp -------------------------------------------------------------------------------- /__tests__/e2e/public/images/ignore-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e/public/images/ignore-img.png -------------------------------------------------------------------------------- /__tests__/e2e/public/images/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e/public/images/img.png -------------------------------------------------------------------------------- /__tests__/e2e/public/images/img.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /__tests__/e2e/public/images/legacy-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e/public/images/legacy-img.png -------------------------------------------------------------------------------- /__tests__/e2e/public/images/picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/__tests__/e2e/public/images/picture.png -------------------------------------------------------------------------------- /__tests__/utils/buildOutputInfo/index.test.ts: -------------------------------------------------------------------------------- 1 | import buildOutputInfo from '../../../src/utils/buildOutputInfo' 2 | import type { Config } from '../../../src/utils/getConfig' 3 | 4 | describe('buildOutputInfo', () => { 5 | test('Default image parser functions properly', () => { 6 | const input = { 7 | src: '/_next/static/media/test.png', 8 | width: 300, 9 | config: {}, 10 | } 11 | 12 | const output = buildOutputInfo(input) 13 | 14 | expect(output).toEqual([ 15 | { 16 | output: '/_next/static/chunks/images/_next/static/media/test_300.webp', 17 | src: '/_next/static/media/test.png', 18 | extension: 'webp', 19 | originalExtension: 'png', 20 | }, 21 | { 22 | output: '/_next/static/chunks/images/_next/static/media/test_300.png', 23 | src: '/_next/static/media/test.png', 24 | extension: 'png', 25 | originalExtension: 'png', 26 | }, 27 | ]) 28 | }) 29 | 30 | test('Default image parser functions properly (remote images)', () => { 31 | const input = { 32 | src: 'https://example.com/images/test.png', 33 | width: 300, 34 | config: {}, 35 | } 36 | 37 | const output = buildOutputInfo(input) 38 | 39 | expect(output).toEqual([ 40 | { 41 | output: '/_next/static/chunks/images/images/test_300.webp', 42 | src: 'https://example.com/images/test.png', 43 | extension: 'webp', 44 | originalExtension: 'png', 45 | }, 46 | { 47 | output: '/_next/static/chunks/images/images/test_300.png', 48 | src: 'https://example.com/images/test.png', 49 | extension: 'png', 50 | originalExtension: 'png', 51 | }, 52 | ]) 53 | }) 54 | 55 | test('config.basePath is applied', () => { 56 | const input = { 57 | src: '/_next/static/media/test.png', 58 | width: 300, 59 | config: { 60 | basePath: '/base-path', 61 | }, 62 | } 63 | 64 | const output = buildOutputInfo(input) 65 | 66 | expect(output).toEqual([ 67 | { 68 | output: '/_next/static/chunks/images/_next/static/media/test_300.webp', 69 | src: '/_next/static/media/test.png', 70 | extension: 'webp', 71 | originalExtension: 'png', 72 | }, 73 | { 74 | output: '/_next/static/chunks/images/_next/static/media/test_300.png', 75 | src: '/_next/static/media/test.png', 76 | extension: 'png', 77 | originalExtension: 'png', 78 | }, 79 | ]) 80 | }) 81 | 82 | test('config.convertFormat is applied', () => { 83 | const input = { 84 | src: '/_next/static/media/test.png', 85 | width: 300, 86 | config: { 87 | convertFormat: [['png', 'webp']], 88 | } as Config, 89 | } 90 | 91 | const output = buildOutputInfo(input) 92 | 93 | expect(output).toEqual([ 94 | { 95 | output: '/_next/static/chunks/images/_next/static/media/test_300.webp', 96 | src: '/_next/static/media/test.png', 97 | extension: 'webp', 98 | originalExtension: 'png', 99 | }, 100 | ]) 101 | }) 102 | 103 | test('config.generateFormats is applied', () => { 104 | const input = { 105 | src: '/_next/static/media/test.png', 106 | width: 300, 107 | config: { 108 | generateFormats: ['avif', 'webp'], 109 | } as Config, 110 | } 111 | 112 | const output = buildOutputInfo(input) 113 | 114 | expect(output).toEqual([ 115 | { 116 | output: '/_next/static/chunks/images/_next/static/media/test_300.avif', 117 | src: '/_next/static/media/test.png', 118 | extension: 'avif', 119 | originalExtension: 'png', 120 | }, 121 | { 122 | output: '/_next/static/chunks/images/_next/static/media/test_300.webp', 123 | src: '/_next/static/media/test.png', 124 | extension: 'webp', 125 | originalExtension: 'png', 126 | }, 127 | { 128 | output: '/_next/static/chunks/images/_next/static/media/test_300.png', 129 | src: '/_next/static/media/test.png', 130 | extension: 'png', 131 | originalExtension: 'png', 132 | }, 133 | ]) 134 | }) 135 | 136 | test('config.generateFormats ignores duplicate extensions', () => { 137 | const input = { 138 | src: '/_next/static/media/test.png', 139 | width: 300, 140 | config: { 141 | generateFormats: ['avif', 'webp', 'avif'], 142 | } as Config, 143 | } 144 | 145 | const output = buildOutputInfo(input) 146 | 147 | expect(output).toEqual([ 148 | { 149 | output: '/_next/static/chunks/images/_next/static/media/test_300.avif', 150 | src: '/_next/static/media/test.png', 151 | extension: 'avif', 152 | originalExtension: 'png', 153 | }, 154 | { 155 | output: '/_next/static/chunks/images/_next/static/media/test_300.webp', 156 | src: '/_next/static/media/test.png', 157 | extension: 'webp', 158 | originalExtension: 'png', 159 | }, 160 | { 161 | output: '/_next/static/chunks/images/_next/static/media/test_300.png', 162 | src: '/_next/static/media/test.png', 163 | extension: 'png', 164 | originalExtension: 'png', 165 | }, 166 | ]) 167 | }) 168 | 169 | test('config.filenameGenerator is applied', () => { 170 | const input = { 171 | src: '/_next/static/media/test.png', 172 | width: 300, 173 | config: { 174 | filenameGenerator: ({ path, name, width, extension }) => `${path}/${name}-${width}.${extension}`, 175 | } as Config, 176 | } 177 | 178 | const output = buildOutputInfo(input) 179 | 180 | expect(output).toEqual([ 181 | { 182 | output: '/_next/static/chunks/images/_next/static/media/test-300.webp', 183 | src: '/_next/static/media/test.png', 184 | extension: 'webp', 185 | originalExtension: 'png', 186 | }, 187 | { 188 | output: '/_next/static/chunks/images/_next/static/media/test-300.png', 189 | src: '/_next/static/media/test.png', 190 | extension: 'png', 191 | originalExtension: 'png', 192 | }, 193 | ]) 194 | }) 195 | 196 | test('config.imageDir and config.externalImageDir is applied', () => { 197 | const input = { 198 | src: '/_next/static/media/test.png', 199 | width: 300, 200 | config: { 201 | imageDir: '/custom/images', 202 | } as Config, 203 | } 204 | 205 | const output = buildOutputInfo(input) 206 | 207 | expect(output).toEqual([ 208 | { 209 | output: '/custom/images/_next/static/media/test_300.webp', 210 | src: '/_next/static/media/test.png', 211 | extension: 'webp', 212 | originalExtension: 'png', 213 | }, 214 | { 215 | output: '/custom/images/_next/static/media/test_300.png', 216 | src: '/_next/static/media/test.png', 217 | extension: 'png', 218 | originalExtension: 'png', 219 | }, 220 | ]) 221 | }) 222 | 223 | test('Unauthorized format in config.convertFormat throws an error', () => { 224 | const input = { 225 | src: '/_next/static/media/test.png', 226 | width: 300, 227 | config: { 228 | convertFormat: [['png', 'invalid_format']], 229 | } as unknown as Config, 230 | } 231 | 232 | expect(() => buildOutputInfo(input)).toThrowError( 233 | new Error('Unauthorized format specified in `configFormat`. afterConvert: invalid_format') 234 | ) 235 | }) 236 | 237 | test('Unauthorized extension in config.generateFormats throws an error', () => { 238 | const input = { 239 | src: '/_next/static/media/test.png', 240 | width: 300, 241 | config: { 242 | generateFormats: ['invalid_format'], 243 | } as unknown as Config, 244 | } 245 | 246 | expect(() => buildOutputInfo(input)).toThrow( 247 | new Error('Unauthorized extension specified in `generateFormats`: invalid_format') 248 | ) 249 | }) 250 | 251 | test('Source image parser in config.sourceImageParser is used', () => { 252 | const customParser = jest.fn(() => ({ 253 | pathWithoutName: 'custom', 254 | name: 'test', 255 | extension: 'png', 256 | })) 257 | 258 | const input = { 259 | src: '/_next/static/media/test.png', 260 | width: 300, 261 | config: { 262 | sourceImageParser: customParser, 263 | }, 264 | } 265 | 266 | const output = buildOutputInfo(input) 267 | 268 | expect(customParser).toHaveBeenCalledWith({ 269 | src: input.src, 270 | defaultParser: expect.any(Function), 271 | }) 272 | 273 | expect(output).toEqual([ 274 | { 275 | output: '/_next/static/chunks/images/custom/test_300.webp', 276 | src: '/_next/static/media/test.png', 277 | extension: 'webp', 278 | originalExtension: 'png', 279 | }, 280 | { 281 | output: '/_next/static/chunks/images/custom/test_300.png', 282 | src: '/_next/static/media/test.png', 283 | extension: 'png', 284 | originalExtension: 'png', 285 | }, 286 | ]) 287 | }) 288 | }) 289 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../dist/cli').run({}) 3 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [ 11 | "./bin", 12 | "./dist", 13 | "./docs", 14 | "./__tests__/**/*.json", 15 | "./__tests__/**/components", 16 | "./bench/**/*.json", 17 | "**/.next", 18 | "**/public", 19 | "**/out", 20 | "./picture.js", 21 | "./picture.d.ts", 22 | "./image.js", 23 | "./image.d.ts", 24 | "./legacy", 25 | "./remote-image.js", 26 | "./remote-image.d.ts", 27 | "./remote-picture.js", 28 | "./remote-picture.d.ts", 29 | "./biome.json" 30 | ] 31 | }, 32 | "formatter": { 33 | "enabled": true, 34 | "indentStyle": "space", 35 | "indentWidth": 2, 36 | "lineEnding": "lf", 37 | "lineWidth": 120, 38 | "attributePosition": "auto", 39 | "bracketSpacing": true 40 | }, 41 | "organizeImports": { 42 | "enabled": true 43 | }, 44 | "linter": { 45 | "enabled": true, 46 | "rules": { 47 | "recommended": true, 48 | "suspicious": { 49 | "noConsole": "off" 50 | } 51 | } 52 | }, 53 | "javascript": { 54 | "formatter": { 55 | "jsxQuoteStyle": "double", 56 | "quoteProperties": "asNeeded", 57 | "trailingCommas": "es5", 58 | "semicolons": "asNeeded", 59 | "arrowParentheses": "always", 60 | "bracketSameLine": false, 61 | "quoteStyle": "single", 62 | "attributePosition": "auto", 63 | "bracketSpacing": true 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /changelog.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a configuration for git-cz. 3 | * 4 | * @see {@link https://github.com/streamich/git-cz#custom-config} for documentation 5 | * @see {@link https://github.com/streamich/git-cz/blob/master/lib/defaults.js} for default configs. 6 | */ 7 | 8 | const types = require('./commit-types.config') 9 | 10 | module.exports = { 11 | /** 12 | * コミットメッセージに絵文字を含めないか 13 | */ 14 | disableEmoji: false, 15 | /** 16 | * コミットメッセージの題目の書式 17 | */ 18 | format: '{type}{scope}: {emoji}{subject}', 19 | /** 20 | * コミット時に選択可能な型 21 | */ 22 | list: types.map(({ type }) => type), 23 | /** 24 | * コミットメッセージ最大文字数 25 | */ 26 | maxMessageLength: 64, 27 | /** 28 | * コミットメッセージ最小文字数 29 | */ 30 | minMessageLength: 3, 31 | /** 32 | * コミット時に入力する項目 33 | */ 34 | questions: [ 35 | 'type', // 型 36 | 'subject', // コミットの題名 37 | 'body', // コミットの本文 38 | 'breaking', // breaking changeの内容 39 | 'issues', // クローズするGitHub issues 40 | ], 41 | /** 42 | * 各型の設定 43 | */ 44 | types: Object.fromEntries(types.map(({ type, description, emoji }) => [type, { description, emoji, value: type }])), 45 | /** 46 | * BREAKING CHANGEに表示する絵文字 47 | */ 48 | breakingChangePrefix: '🧨', 49 | /** 50 | * Closesに表示する絵文字 51 | */ 52 | closedIssuePrefix: '✅', 53 | } 54 | -------------------------------------------------------------------------------- /commit-types.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} Type conventional commitsの型の設定 3 | * @property {string} type conventional commitsの型 4 | * @property {"major" | "minor" | "patch" | undefined } [release] セマンティックバージョンの上げ方。undefinedはリリースされません。 5 | * @property {string} description git-czに表示される説明文 6 | * @property {string} emoji git-czで使う絵文字 7 | * @property {string | undefined} [section] changelogに表示する見出し 8 | * @property {boolean | undefined} [hidden] changelogに表示するか否か 9 | */ 10 | 11 | /** 12 | * @type {Array.} 13 | */ 14 | module.exports = [ 15 | { 16 | type: 'feat', // 機能追加 17 | release: 'minor', 18 | description: 'A new feature', 19 | emoji: '🚀', 20 | section: 'Features', 21 | hidden: false, 22 | }, 23 | { 24 | type: 'fix', // バグ修正 25 | release: 'patch', 26 | description: 'A bug fix', 27 | emoji: '🐛', 28 | section: 'Bug Fixes', 29 | hidden: false, 30 | }, 31 | { 32 | type: 'sec', // 脆弱性の解消 33 | release: 'patch', 34 | description: 'A vulnerability fix', 35 | emoji: '👮‍', 36 | section: 'Security', 37 | hidden: false, 38 | }, 39 | { 40 | type: 'perf', // パフォーマンスのみの改善 41 | release: 'patch', 42 | description: 'A code change that improves performance', 43 | emoji: '⚡️', 44 | section: 'Performance Improvements', 45 | hidden: false, 46 | }, 47 | { 48 | type: 'refactor', // 機能追加やバグ修正を伴わないリファクタリング 49 | release: undefined, 50 | description: 'A code change that neither fixes a bug or adds a feature', 51 | emoji: '💡', 52 | section: 'Code Refactoring', 53 | hidden: true, 54 | }, 55 | { 56 | type: 'docs', // ドキュメントのみの変更 57 | release: undefined, 58 | description: 'Documentation only changes', 59 | emoji: '✏️', 60 | section: 'Documentation', 61 | hidden: false, 62 | }, 63 | { 64 | type: 'release', // リリースコミット 65 | release: undefined, 66 | description: 'Create a release commit', 67 | emoji: '🏹', 68 | hidden: true, 69 | }, 70 | { 71 | type: 'style', // コーディングスタイル関連の修正 72 | release: undefined, 73 | description: 'Markup, white-space, formatting, missing semi-colons...', 74 | emoji: '💄', 75 | section: 'Styles', 76 | hidden: true, 77 | }, 78 | { 79 | type: 'test', // テストの追加変更 80 | release: undefined, 81 | description: 'Adding missing tests', 82 | emoji: '💍', 83 | section: 'Tests', 84 | hidden: false, 85 | }, 86 | { 87 | type: 'ci', // CI関連の変更 88 | release: undefined, 89 | description: 'CI related changes', 90 | emoji: '🎡', 91 | section: 'Continuous Integration', 92 | hidden: false, 93 | }, 94 | { 95 | type: 'chore', // ビルドプロセスや補助ツールの変更 96 | release: undefined, 97 | description: 'Build process or auxiliary tool changes', 98 | emoji: '🤖', 99 | section: 'Chore', 100 | hidden: true, 101 | }, 102 | ] 103 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A configuration for commitlint. 3 | * @see {@link https://commitlint.js.org/#/} for details. 4 | */ 5 | 6 | const typeEnum = require('./commit-types.config').map(({ type }) => type) 7 | const scopeEnum = require('./changelog.config').scopes 8 | const subjectMinLength = require('./changelog.config').minMessageLength ?? 3 9 | const subjectMaxLength = require('./changelog.config').maxMessageLength ?? 64 10 | 11 | module.exports = { 12 | extends: ['@commitlint/config-conventional'], 13 | /** 14 | * @see {@link https://commitlint.js.org/#/reference-rules} for rule details. 15 | */ 16 | rules: { 17 | 'type-enum': [2, 'always', typeEnum], 18 | 'scope-enum': [2, 'always', scopeEnum], 19 | 'subject-min-length': [2, 'always', subjectMinLength], 20 | 'subject-max-length': [2, 'always', subjectMaxLength], 21 | 'body-max-length': [0], // releaseコミットがエラーになるため無効化する 22 | 'body-max-line-length': [0], // releaseコミットがエラーになるため無効化する 23 | 'footer-max-length': [0], // releaseコミットがエラーになるため無効化する 24 | 'footer-max-line-length': [0], // releaseコミットがエラーになるため無効化する 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | -------------------------------------------------------------------------------- /docs/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | semi: false, 5 | singleQuote: true, 6 | printWidth: 120, 7 | } 8 | -------------------------------------------------------------------------------- /docs/.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 22.16.0 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ npm i 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ npm start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ npm run build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/01-intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This page provides background on the creation of the library and its features. 3 | --- 4 | 5 | # Introduction 6 | 7 | Next.js is a very good framework and has become indispensable for web development. 8 | That is exactly why, once people get used to this developer experience, they may not want to develop without Next.js. 9 | (I am one of them lol.) 10 | 11 | However, Next.js' export functionality also has its limitations, the most discussed of which is **image optimization**. 12 | https://github.com/vercel/next.js/discussions/19065 13 | 14 | Using this repository, you can get the full benefits of `next/image` even when using `next export` by doing image optimization at build time. 15 | 16 | This makes it possible to build a high performance website with this solution, whether you want to build a simple website or a completely static output. 17 | 18 | ## Feature 19 | 20 | - Optimize images at build time. 21 | - All options for `next/image` available 22 | - Convert formats (png → webp, etc.) 23 | - Download external images locally. 24 | - Using `sharp`, so it's fast. 25 | - Cache prevents repeating the same optimization 26 | - Support TypeScript 27 | - Support AppRouter 28 | -------------------------------------------------------------------------------- /docs/docs/02-getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This page introduces how to get started with this library. 3 | --- 4 | 5 | # Getting Started 6 | 7 | Install the package in the project that uses Next.js. 8 | 9 | ## Installation 10 | 11 | ```bash 12 | npm i -D next-export-optimize-images 13 | ``` 14 | 15 | ## Usage 16 | 17 | 1. Write withExportImages in `next.config.js.` 18 | 19 | ```js title="next.config.js" 20 | const withExportImages = require('next-export-optimize-images') 21 | 22 | module.exports = withExportImages({ 23 | output: 'export', 24 | // write your next.js configuration values. 25 | }) 26 | ``` 27 | 28 | Alternatively, if another plugin is used in conjunction, write 29 | 30 | ```js title="next.config.js" 31 | const withExportImages = require('next-export-optimize-images') 32 | const withAnalyzer = require('@next/bundle-analyzer')({ 33 | enabled: process.env.ANALYZE === 'true', 34 | }) 35 | 36 | module.exports = withExportImages( 37 | withAnalyzer({ 38 | output: 'export', 39 | // write your next.js configuration values. 40 | }) 41 | ) 42 | // Or 43 | module.exports = async () => { 44 | const config = await withExportImages({ 45 | output: 'export', 46 | // write your next.js configuration values. 47 | }) 48 | 49 | return withAnalyzer(config) 50 | } 51 | ``` 52 | 53 | Or ES modules can be used. 54 | 55 | ```js title="next.config.mjs" 56 | import withExportImages from 'next-export-optimize-images' 57 | 58 | export default withExportImages({ 59 | output: 'export', 60 | // write your next.js configuration values. 61 | }) 62 | ``` 63 | 64 | :::note 65 | 66 | `withExportImages` is an asynchronous function that returns a Promise, so either write `withExportImages` first or wait for the Promise to resolve before applying other plugins. 67 | 68 | ::: 69 | 70 | 2. Change the description of the `scripts` that do the `next build` in `package.json` 71 | 72 | ```diff title="package.json" 73 | { 74 | - "build": "next build", 75 | + "build": "next build && next-export-optimize-images", 76 | } 77 | ``` 78 | 79 | 3. Import from `next-export-optimize-images/image` and use it. 80 | 81 | ```tsx 82 | import Image from 'next-export-optimize-images/image' 83 | 84 | 85 | // Or import as follows 86 | import img from './img.png' 87 | 88 | // Or require as follows 89 | 90 | ``` 91 | 92 | Alternatively, you can use `next/legacy/image`. 93 | 94 | ```tsx 95 | import Image from 'next-export-optimize-images/legacy/image' 96 | 97 | 98 | // Or import as follows 99 | import img from './img.png' 100 | 101 | // Or require as follows 102 | 103 | ``` 104 | 105 | ## Local checks 106 | 107 | 1. Run `npm run build`. 108 | 2. Run `npx http-server out` 109 | -------------------------------------------------------------------------------- /docs/docs/03-Features/01-picture-component.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This page introduces the Picture component for multiple image formats. 3 | --- 4 | 5 | # Picture component 6 | 7 | When pre-generating images, it is usually not possible to support multiple image formats using only the img tag. 8 | By using the `Picture` component provided by this library, multiple image formats can be supported. 9 | 10 | ## Usage 11 | 12 | ```tsx 13 | import Picture from 'next-export-optimize-images/picture' 14 | 15 | function Component() { 16 | return ( 17 | <> 18 | 19 | {/* 20 | Or import as follows 21 | import img from './img.png' 22 | 23 | */} 24 | 25 | ) 26 | } 27 | ``` 28 | 29 | At this time, the output is as follows (Some parts are omitted.) 30 | 31 | ```html 32 | 33 | 37 | 46 | 47 | ``` 48 | 49 | ## Definition 50 | 51 | - props: `ImageProps` 52 | - return: `JSX.Element` 53 | 54 | ※ `ImageProps` is the same as the props of the `Image` component provided by `next/image`. 55 | 56 | ## Advanced usage 57 | 58 | By default, two image formats are output: the image format specified in src and webp. 59 | If a webp image is specified in src, only the webp image will be output. 60 | 61 | You can enable AVIF support with the following configuration. 62 | 63 | ```js title="export-images.config.js" 64 | /** 65 | * @type {import('next-export-optimize-images').Config} 66 | */ 67 | const config = { 68 | generateFormats: ['avif', 'webp'], 69 | } 70 | 71 | module.exports = config 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/docs/03-Features/02-remote-image-component.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This page introduces the RemoteImage component for remote images. 3 | --- 4 | 5 | # RemoteImage component 6 | 7 | If you want to use remote images (external images) in this library, you need to manually write the external image URL in the configuration file or use the `RemoteImage` component. 8 | In other words, using the `RemoteImage` component eliminates the need to manually write the image URL in the configuration file. 9 | 10 | ## Usage 11 | 12 | ```tsx 13 | import RemoteImage from 'next-export-optimize-images/remote-image' 14 | 15 | function Component() { 16 | return ( 17 | <> 18 | 19 | {/* 20 | Or use dynamic values with variables 21 | const id = 'image01' 22 | 23 | */} 24 | 25 | ) 26 | } 27 | ``` 28 | 29 | or Picture tag. 30 | (webp support is added by default) 31 | 32 | ```tsx 33 | import RemotePicture from 'next-export-optimize-images/remote-picture' 34 | 35 | function Component() { 36 | return ( 37 | <> 38 | 39 | {/* 40 | Or use dynamic values with variables 41 | const id = 'image01' 42 | 43 | */} 44 | 45 | ) 46 | } 47 | ``` 48 | 49 | ## Definition 50 | 51 | - props: `Omit & { src: string }` 52 | - return: `JSX.Element` 53 | 54 | ※ ImageProps is the same as the props of the Image component provided by next/image. 55 | 56 | ## Tips 57 | 58 | ### Use with `remoteImages`. 59 | 60 | ```js title="export-images.config.js" 61 | /** 62 | * @type {import('next-export-optimize-images').Config} 63 | */ 64 | const config = { 65 | remoteImages: ['https://example.com/image01.jpg', 'https://example.com/image02.jpg'], 66 | } 67 | 68 | module.exports = config 69 | ``` 70 | 71 | ```tsx 72 | import Image from 'next-export-optimize-images/image' 73 | import RemoteImage from 'next-export-optimize-images/remote-image' 74 | 75 | function Component() { 76 | return ( 77 | <> 78 | 79 | 80 | 81 | 82 | ) 83 | } 84 | ``` 85 | 86 | 'image01.jpg', 'image02.jpg' and 'image03.jpg' are downloaded locally and optimized. 87 | -------------------------------------------------------------------------------- /docs/docs/03-Features/03-build-mode.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This page introduces the build mode. How to build with next build && next start without using output 'export'. 3 | --- 4 | 5 | # Build Mode 6 | 7 | Build mode allows you to pre-optimize images even if you are using `next build` and `next start`. This allows you to run your application on the Next.js server while pre-optimizing only the images. 8 | 9 | ## Usage 10 | 11 | To use build mode, you need to set the `mode` option to `'build'` in the `export-images.config.js` file. 12 | 13 | ```js title="export-images.config.js" 14 | /** 15 | * @type {import('next-export-optimize-images').Config} 16 | */ 17 | const config = { 18 | mode: 'build', 19 | } 20 | 21 | module.exports = config 22 | ``` 23 | 24 | Then execute the normal build commands. 25 | 26 | ```sh 27 | next build && next-export-optimize-images 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/docs/03-Features/04-get-props.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This page introduces the `getOptimizedImageProps` function, which is used to get the props of a component. 3 | --- 4 | 5 | # Get Optimized Image Props 6 | 7 | `getOptimizedImageProps` is a function that extracts the internal processing of the `Image` component provided by this library. 8 | 9 | For example, if you use the `` element directly without using the `Image` component, you can use `getOptimizedImageProps` to get the properties to pass to the `` element. 10 | This allows for more advanced use cases such as art direction using the `` element or displaying images using the CSS `background-image` property. 11 | 12 | ## Usage 13 | 14 | ### Background image 15 | 16 | ```tsx 17 | 'use client' 18 | 19 | import { getOptimizedImageProps } from 'next-export-optimize-images/image' 20 | 21 | import src from '../images/sample.png' 22 | 23 | export default function BackgroundImage() { 24 | const props = getOptimizedImageProps({ src, alt: '' }).props 25 | 26 | return ( 27 |
34 | ) 35 | } 36 | 37 | export default WithPropsComponent 38 | ``` 39 | 40 | ### Art direction 41 | 42 | ```tsx 43 | 'use client' 44 | 45 | import { getOptimizedImageProps } from 'next-export-optimize-images/image' 46 | 47 | import srcDesktop from '../images/sample-desktop.png' 48 | import srcMobile from '../images/sample-mobile.png' 49 | 50 | export default function BackgroundImage() { 51 | const propsDesktop = getOptimizedImageProps({ src: srcDesktop, alt: '' }).props 52 | const propsMobile = getOptimizedImageProps({ src: srcMobile, alt: '' }).props 53 | 54 | return ( 55 | 56 | 62 | 63 | 64 | ) 65 | } 66 | 67 | export default WithPropsComponent 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/docs/03-Features/05-external-images.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This page introduces the handling of external images. 3 | --- 4 | 5 | # External images 6 | 7 | This library can also handle external images. 8 | This, like the other features, works at build time and does not affect development speed. 9 | 10 | :::tip 11 | 12 | In most cases, the use of the RemoteImage or RemotePicture component is recommended. 13 | [Docs: RemoteImage component](./remote-image-component) 14 | 15 | ::: 16 | 17 | ## Usage 18 | 19 | ```jsx 20 | 21 | ``` 22 | 23 | Need to add a setting to `export-images.config.js` as follows. 24 | 25 | ```js title="export-images.config.js" 26 | module.exports = { 27 | remoteImages: ['https://next-export-optimize-images.vercel.app/og.png'], 28 | // remoteImages: async () => { 29 | // const imageUrls = await getImageUrls() // get image urls from CMS, etc. 30 | // return imageUrls 31 | // } 32 | } 33 | ``` 34 | 35 | When in production, it will be rendered as follows. (Only important parts are shown.) 36 | 37 | ```jsx 38 | 42 | ``` 43 | 44 | During development, as with local images, no optimization is performed. 45 | Also, no downloading to local is performed. 46 | 47 | ```jsx 48 | 52 | ``` 53 | 54 | ### `remoteImagesDownloadsDelay` 55 | 56 | - Type: number 57 | 58 | In case you need to download a large amount of images from an external CDN with a rate limit, this will add delays between downloading images. 59 | 60 | effectively this will add `sleep` function between downloads. 61 | -------------------------------------------------------------------------------- /docs/docs/03-Features/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": 3, 3 | "className": "red", 4 | "link": { 5 | "type": "generated-index", 6 | "title": "Features", 7 | "description": "List of Main Features." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/docs/04-Configurations/01-basic-configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This page introduces how to change the behavior of this library. 3 | --- 4 | 5 | # Basic Configuration 6 | 7 | Default behavior can be changed as needed. 8 | Create `export-images.config.js` in the root. 9 | 10 | ```js title="export-images.config.js" 11 | /** 12 | * @type {import('next-export-optimize-images').Config} 13 | */ 14 | const config = { 15 | // your configuration values. 16 | } 17 | 18 | module.exports = config 19 | ``` 20 | 21 | ## Optional fields 22 | 23 | ### `outDir` 24 | 25 | - Type: string 26 | - Default: 'out' 27 | 28 | Specify if you are customizing the default output directory, such as `next export -o outDir`. 29 | 30 | ### `imageDir` 31 | 32 | - Type: string 33 | - Default: '\_next/static/chunks/images' 34 | 35 | You can customize the directory to output optimized images. 36 | The default is `'_next/static/chunks/images'`. 37 | 38 | e.g. If `'_optimized'` is set. 39 | 40 | ```diff 41 | - out/_next/static/chunks/images/filename.png 42 | + out/_optimized/filename.png 43 | ``` 44 | 45 | ### `cacheDir` 46 | 47 | - Type: string 48 | - Default: 'node_modules/.cache' 49 | 50 | You can customize the directory to cache the optimized images. 51 | The default is `node_modules/.cache`. 52 | 53 | ### `ignorePaths` 54 | 55 | - Type: Array<string\> 56 | - Default: [] 57 | 58 | Images in the public directory are automatically optimized, but if there are any images you want to ignore the optimization for, please specify the path. 59 | Please specify a relative path from the public directory. 60 | 61 | ### `externalImageDir` 62 | 63 | - Type: string 64 | - Default: '\_next/static/media' 65 | 66 | You can customize the directory to output downloaded external images. 67 | The default is `'_next/static/media'`. 68 | 69 | ### `quality` 70 | 71 | - Type: number 72 | - Default: 75 73 | 74 | You can customize the quality of the optimized image. 75 | 76 | ### `basePath` 77 | 78 | - Type: string 79 | - Default: '' 80 | 81 | Required if you have set `basePath` in `next.config.js`. 82 | Please set the same value. 83 | 84 | ### `filenameGenerator` 85 | 86 | - Type: function 87 | - Argument: Object 88 | - Return value: string 89 | 90 | | Key | Type | Description | e.g. '/images/sample.png' | e.g. require('./sample.png') | 91 | | --------- | ------ | -------------------------------- | ------------------------- | ---------------------------- | 92 | | path | string | The path portion. | /images | /\_next/static/media | 93 | | name | string | The file name part. | sample | sample.{hash} | 94 | | width | number | That image is the resized width. | 1920 | 1920 | 95 | | extension | string | The extension of that image. | png | png | 96 | 97 | You can customize the generation of file names. 98 | 99 | e.g. '/images/sample.png' 100 | 101 | ```js 102 | const config = { 103 | filenameGenerator: ({ path, name, width, extension }) => 104 | `${path.replace(/^\//, '').replace(/\//g, '-')}-${name}.${width}.${extension}`, 105 | } 106 | ``` 107 | 108 | ```diff 109 | - '/images/sample_1920_75.png' 110 | + 'images-sample.1920.75.png' 111 | ``` 112 | 113 | #### ❗️Attention 114 | 115 | When making this setting, make sure that the file names (including the path part) of different images do not cover each other. 116 | Specifically, include the name, width, quality, and extension in the return value. If path is not included, all src's should be specified with `import` or `require` so that they can be distinguished by their hash value even if they have the same filename. 117 | 118 | ### `sourceImageParser` 119 | 120 | - Type: function 121 | - Argument: Object 122 | - Return value: ParsedImageInfo 123 | 124 | The argument for this function will be an object with the following shape: 125 | 126 | ```typescript 127 | { 128 | src: string // The source images 'src' attribute 129 | defaultParser: (src: string) => ParsedImageInfo // A function which evaluates the image name, path name (without image name appended and starting w/ '/'), and extension 130 | } 131 | ``` 132 | 133 | The return value for this function will be an object with the following shape (ParsedImageInfo type): 134 | 135 | ```typescript 136 | { 137 | pathWithoutName: string // The image path (not including the image name) 138 | name: string // The image name 139 | extension: string // The image extension 140 | } 141 | ``` 142 | 143 | This might be useful if any of your images have URLs that do not follow the standard `https://somehost.com/imagename.extension` pattern. 144 | 145 | For example: Maybe your source image's src attribute is more like `https://somedigitalassetmangementhost.com?fileId=1234-xyze&extension=jpg`, so you might add the following to your config: 146 | 147 | **NOTE** 148 | This gets run before filenameGenerator, so the arguments passed into filenameGenerator would be affected by sourceImageParser configuration. (path, name, and extension) 149 | 150 | ```typescript 151 | // export-images.config.js 152 | /** 153 | * @type {import('next-export-optimize-images').Config} 154 | */ 155 | const config = { 156 | sourceImageParser: ({ src, defaultParser }) => { 157 | const regExpMatches = src.match(/^.*\?fileId=(.*)&extension=(\w*).*$/) 158 | if (!regExpMatches) { 159 | return defaultParser(src) 160 | } 161 | 162 | // if the src has fileId and extension in its route then it 163 | // must be a non-standard image, so parse it differently for all intents 164 | // and purposes 165 | return { 166 | pathWithoutName: '', // maybe there is no path, or you can supply an arbitrary one for filename processing 167 | name: regExpMatches[1] || '', 168 | extension: regExpMatches[2] || '', 169 | } 170 | }, 171 | } 172 | ``` 173 | 174 | ### `sharpOptions` 175 | 176 | - Type: Object 177 | 178 | | Key | Description | 179 | | ---- | ----------------------------------------------- | 180 | | png | https://sharp.pixelplumbing.com/api-output#png | 181 | | jpg | https://sharp.pixelplumbing.com/api-output#jpeg | 182 | | webp | https://sharp.pixelplumbing.com/api-output#webp | 183 | | avif | https://sharp.pixelplumbing.com/api-output#avif | 184 | 185 | You can set optimization options for each extension. 186 | Please refer to the official sharp documentation for more information. 187 | 188 | ### `convertFormat` 189 | 190 | - Type: Array<Array<Format, Format>> 191 | Format → "jpeg" | "jpg" | "png" | "webp" | "avif" 192 | 193 | :::note 194 | 195 | This option was developed before the `Picture` component was added yet. It is now recommended that the Picture component be used to allow multiple image formats to be displayed. 196 | 197 | See [Picture component](/docs/Features/picture-component) 198 | 199 | ::: 200 | 201 | It allows you to convert images from any extension to another extension. 202 | 203 | e.g. 204 | 205 | ```js 206 | const config = { 207 | convertFormat: [ 208 | ['png', 'webp'], 209 | ['jpg', 'avif'], 210 | ], 211 | } 212 | ``` 213 | 214 | ```jsx 215 | 216 | 217 | ``` 218 | 219 | The original image will be kept, `img.png` will be converted to webp format and `img.jpg` will be converted to avif format and output to the directory. 220 | 221 | ### `generateFormats` 222 | 223 | - Type: Array<"webp" | "avif"\> 224 | - Default: ['webp'] 225 | 226 | You can generate extra images in extensions specified. 227 | 228 | This setting affects the extension displayed in the `Picture` component. 229 | The order is also important. 230 | For example, if `webp` is first, then `webp` will be displayed first. 231 | 232 | See [Picture component](/docs/Features/picture-component) for details. 233 | 234 | ### `remoteImages` 235 | 236 | - Type: Array<string\> | (() => Array<string\> | Promise<Array<string\>\>) 237 | 238 | You can directly specify the URL of an external image. 239 | This is useful in cases where it is not known what images will be used for the build using variables, [for example](/docs/Features/external-images#when-specifying-an-external-image-url-with-a-variable). 240 | 241 | ### `remoteImagesDownloadsDelay` 242 | 243 | - Type: number 244 | 245 | In case you need to download a large amount of images from an external CDN with a rate limit, this will add delays between downloading images. 246 | 247 | ### `mode` 248 | 249 | - Type: 'build' | 'export' 250 | - Default: 'export' 251 | 252 | 'build' mode is for use with `next build` and `next start`. 253 | 254 | [Document page](/docs/Features/build-mode) 255 | -------------------------------------------------------------------------------- /docs/docs/04-Configurations/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": 4, 3 | "className": "red", 4 | "link": { 5 | "type": "generated-index", 6 | "title": "Configuration", 7 | "description": "How to change the default behavior." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/docs/05-comparison.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This page presents a comparison with other similar libraries. 3 | --- 4 | 5 | # Comparison with similar libraries 6 | 7 | It is very difficult to write this page objectively, but on the other hand I understand that it is very important for developers. 8 | 9 | We will do our best to avoid bias and provide fair and accurate information, but if you find something wrong or inaccurate, please let us know by submitting an Issue! 10 | 11 | ## next-optimized-images 12 | 13 | https://github.com/cyrilwanner/next-optimized-images 14 | 15 | It is probably the most famous of Next.js' image optimization libraries. 16 | Compared to our library, it has the following characteristics 17 | 18 | - Optimize images using `webpack`'s `loader` feature. 19 | - No need to use `next/image` 20 | 21 | Let me list some of the disadvantages. 22 | 23 | - Update stopped on August 9, 2020. 24 | - Bundle size is bloated when long strings such as srcSet are needed to bundle images with webpack. 25 | 26 | Because of the above features, we would like to compare our library with this one and recommend our library to the following users. 27 | 28 | - **I want to use `next/image` to optimize images.** 29 | - **I'm using `responsive-loader`, but I'm concerned about the bundle size of images.** 30 | 31 | ## next-image-export-optimizer 32 | 33 | https://github.com/Niels-IO/next-image-export-optimizer 34 | 35 | Since this library is very similar to ours, it would be very good for you to try this one as well. 36 | 37 | A brief comparison with our library reveals the following characteristics for your reference. 38 | 39 | - Specify a directory, such as `public/images`, and the images in it will be processed. 40 | 41 | Let me list some of the disadvantages. 42 | 43 | - Settings are somewhat complicated and cumbersome. 44 | - All options for `next/image` are not available. 45 | - Only one extension can be handled. 46 | 47 | Due to the above features, `next-image-export-optimizer` is not recommended for the following users. 48 | 49 | - Want to use it as simply as possible. 50 | - When multiple formats of images are required to be supported by Picture component. 51 | - I want to use `next build` to optimize images in advance while using the Node.js server. 52 | - Remote image optimization made easy. 53 | 54 | ## Please let me know if there are others! 55 | 56 | If you know of any other similar libraries, please let us know [here](https://github.com/dc7290/next-export-optimize-images/issues/new)! 57 | I'll cry and be happy lol.😂 58 | -------------------------------------------------------------------------------- /docs/docs/06-examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This page is to introduce examples of use. 3 | --- 4 | 5 | # Examples 6 | 7 | ## Use with next/image 8 | 9 | In this section, we will introduce how to combine “image optimization during build time using next-export-optimize-images” and “image optimization on demand using next/image”. 10 | 11 | ```js title="next.config.js" 12 | const withExportImages = require('next-export-optimize-images') 13 | 14 | module.exports = withExportImages({ 15 | output: 'export', 16 | images: { 17 | loader: 'default', 18 | }, 19 | // write your next.js configuration values. 20 | }) 21 | ``` 22 | 23 | ```jsx 24 | import NextImage from 'next/image' 25 | import Image from 'next-export-optimize-images/image' 26 | 27 | export default function Home() { 28 | return ( 29 |
30 | 31 | 32 |
33 | ) 34 | } 35 | ``` 36 | 37 | ## Set the `deviceSizes` 38 | 39 | ```js title="next.config.js" 40 | module.exports = withExportImages({ 41 | images: { 42 | deviceSizes: [640, 960, 1280, 1600, 1920], 43 | }, 44 | }) 45 | ``` 46 | 47 | ## Set the `placeholder` 48 | 49 | ```jsx 50 | 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/docs/07-qa.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This page is for Q&A 3 | --- 4 | 5 | # Q&A 6 | 7 | ## Can I reduce build time? 8 | 9 | First, please check the specifications of your PC. 10 | The standard is based on machines with 8 or more cores as recommended by SHARP, especially those with large L1/L2 CPU caches. 11 | 12 | And if the build still takes a long time, try the following settings. 13 | 14 | ### Reduce image width to be optimized 15 | 16 | Reduce the image width generated by next/image as follows. 17 | 18 | ```js title="next.config.js" 19 | module.exports = withExportImages({ 20 | images: { 21 | deviceSizes: [640, 960, 1280, 1600, 1920], 22 | }, 23 | }) 24 | ``` 25 | 26 | ### Reduce effort with `sharp` options 27 | 28 | ```js title="export-images.config.js" 29 | module.exports = { 30 | sharpOptions: { 31 | png: { 32 | effort: 1, 33 | }, 34 | webp: { 35 | effort: 0, 36 | }, 37 | avif: { 38 | effort: 0, 39 | }, 40 | }, 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/docs/08-planned-features.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This page is to introduce examples of use. 3 | draft: true 4 | --- 5 | 6 | # Planned Features 7 | 8 | :::caution 9 | There API is in the planning stage and is subject to change in practice. 10 | ::: 11 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | const { themes } = require('prism-react-renderer') 2 | const lightCodeTheme = themes.github 3 | const darkCodeTheme = themes.dracula 4 | 5 | /** @type {import('@docusaurus/types').Config} */ 6 | const config = { 7 | title: 'Next Export Optimize Images', 8 | url: 'https://next-export-optimize-images.vercel.app', 9 | baseUrl: '/', 10 | onBrokenLinks: 'throw', 11 | onBrokenMarkdownLinks: 'warn', 12 | 13 | // GitHub pages deployment config. 14 | // If you aren't using GitHub pages, you don't need these. 15 | organizationName: 'dc7290', // Usually your GitHub org/user name. 16 | projectName: 'next-export-optimize-images', // Usually your repo name. 17 | 18 | // Even if you don't use internalization, you can use this field to set useful 19 | // metadata like html lang. For example, if your site is Chinese, you may want 20 | // to replace "en" with "zh-Hans". 21 | i18n: { 22 | defaultLocale: 'en', 23 | locales: ['en'], 24 | }, 25 | 26 | presets: [ 27 | [ 28 | 'classic', 29 | /** @type {import('@docusaurus/preset-classic').Options} */ 30 | ({ 31 | docs: { 32 | sidebarPath: require.resolve('./sidebars.js'), 33 | // Please change this to your repo. 34 | // Remove this to remove the "edit this page" links. 35 | editUrl: 'https://github.com/dc7290/next-export-optimize-images/tree/main/docs/', 36 | }, 37 | theme: { 38 | customCss: require.resolve('./src/css/custom.css'), 39 | }, 40 | }), 41 | ], 42 | ], 43 | 44 | themeConfig: 45 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 46 | ({ 47 | navbar: { 48 | title: 'Next Export Optimize Images', 49 | items: [ 50 | { 51 | type: 'doc', 52 | docId: 'intro', 53 | position: 'left', 54 | label: 'Docs', 55 | }, 56 | { 57 | href: 'https://github.com/dc7290/next-export-optimize-images', 58 | label: 'GitHub', 59 | position: 'right', 60 | }, 61 | ], 62 | }, 63 | footer: { 64 | style: 'dark', 65 | links: [ 66 | { 67 | title: 'General resources', 68 | items: [ 69 | { 70 | label: 'Docs', 71 | to: '/docs/intro', 72 | }, 73 | ], 74 | }, 75 | { 76 | title: 'More', 77 | items: [ 78 | { 79 | label: 'GitHub', 80 | href: 'https://github.com/dc7290/next-export-optimize-images', 81 | }, 82 | { 83 | label: 'Releases', 84 | href: 'https://github.com/dc7290/next-export-optimize-images/releases', 85 | }, 86 | ], 87 | }, 88 | { 89 | title: 'About me', 90 | items: [ 91 | { 92 | label: 'Github', 93 | href: 'https://github.com/dc7290', 94 | }, 95 | { 96 | label: 'Twitter', 97 | href: 'https://twitter.com/d_suke_09', 98 | }, 99 | ], 100 | }, 101 | ], 102 | copyright: 'Copyright © 2022 dc7290. Built with Docusaurus.', 103 | }, 104 | image: '/og.png', 105 | colorMode: { 106 | disableSwitch: true, 107 | respectPrefersColorScheme: true, 108 | }, 109 | prism: { 110 | theme: lightCodeTheme, 111 | darkTheme: darkCodeTheme, 112 | }, 113 | algolia: { 114 | appId: 'FK0H7QU600', 115 | apiKey: '022584b659d9a91bb9bb8f06fe859d6d', 116 | indexName: 'next-export-optimize-images', 117 | searchPagePath: 'search', 118 | }, 119 | }), 120 | plugins: [ 121 | async function myPlugin() { 122 | return { 123 | name: 'docusaurus-tailwindcss', 124 | configurePostCss(postcssOptions) { 125 | // Appends TailwindCSS and AutoPrefixer. 126 | postcssOptions.plugins.push(require('tailwindcss')) 127 | postcssOptions.plugins.push(require('autoprefixer')) 128 | return postcssOptions 129 | }, 130 | } 131 | }, 132 | ], 133 | } 134 | 135 | module.exports = config 136 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "scripts": { 5 | "build": "docusaurus build", 6 | "clear": "docusaurus clear", 7 | "deploy": "docusaurus deploy", 8 | "docusaurus": "docusaurus", 9 | "serve": "docusaurus serve", 10 | "start": "docusaurus start", 11 | "swizzle": "docusaurus swizzle", 12 | "write-heading-ids": "docusaurus write-heading-ids", 13 | "write-translations": "docusaurus write-translations" 14 | }, 15 | "browserslist": { 16 | "production": [ 17 | ">0.5%", 18 | "not dead", 19 | "not op_mini all" 20 | ], 21 | "development": [ 22 | "last 1 chrome version", 23 | "last 1 firefox version", 24 | "last 1 safari version" 25 | ] 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/core": "^3.8.1", 29 | "@docusaurus/preset-classic": "^3.8.1", 30 | "@heroicons/react": "2.2.0", 31 | "@mdx-js/react": "3.1.0", 32 | "autoprefixer": "10.4.21", 33 | "clsx": "2.1.1", 34 | "postcss": "8.5.4", 35 | "prettier": "3.5.3", 36 | "prettier-plugin-tailwindcss": "0.6.12", 37 | "prism-react-renderer": "2.4.1", 38 | "react": "18.3.1", 39 | "react-dom": "18.3.1", 40 | "tailwindcss": "3.4.17" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['hello'], 26 | }, 27 | ], 28 | */ 29 | } 30 | 31 | module.exports = sidebars 32 | -------------------------------------------------------------------------------- /docs/src/components/Features.jsx: -------------------------------------------------------------------------------- 1 | import { CheckCircleIcon, CubeIcon, BoltIcon } from '@heroicons/react/24/outline' 2 | import React from 'react' 3 | 4 | const features = [ 5 | { 6 | name: 'Optimize images at build time', 7 | description: 8 | 'Normally, to use `next/image` with `next export`, you need to use a cloud provider for image optimization. With this solution, however, you can optimize images at build time, eliminating the need for a cloud provider.', 9 | icon: CubeIcon, 10 | }, 11 | { 12 | name: 'All options for `next/image` available', 13 | description: 14 | 'There is no need to use any special components. Use `next/image` as usual, all its options are available.', 15 | icon: CheckCircleIcon, 16 | }, 17 | { 18 | name: "Using `sharp`, so it's fast.", 19 | description: 20 | 'It is fast because it uses `sharp` for image optimization. This is also the approach used in Next.js, which is much faster than other image processing libraries.', 21 | icon: BoltIcon, 22 | }, 23 | { 24 | name: 'Cache prevents repeating the same optimization', 25 | description: 'It has an internal cache mechanism so that the same images are not optimized repeatedly.', 26 | icon: CubeIcon, 27 | }, 28 | ] 29 | 30 | const Features = () => { 31 | return ( 32 |
33 |
34 | 56 | 57 |
58 |
59 |

60 | Evolve Next.js into a complete static site generator. 61 |

62 |
63 |
64 | {features.map((feature) => ( 65 |
66 |
67 |
68 |
70 |

{feature.name}

71 |
72 |
{feature.description}
73 |
74 | ))} 75 |
76 |
77 |
78 |
79 | ) 80 | } 81 | 82 | export default Features 83 | -------------------------------------------------------------------------------- /docs/src/components/Hero.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Text from '../components/Text' 4 | 5 | const Hero = () => { 6 | return ( 7 |
8 |
9 |
12 |

Optimize images at build time with Next.js.

13 |
14 | ) 15 | } 16 | 17 | export default Hero 18 | -------------------------------------------------------------------------------- /docs/src/components/Introduction.jsx: -------------------------------------------------------------------------------- 1 | import Link from '@docusaurus/Link' 2 | import { ChevronRightIcon } from '@heroicons/react/24/outline' 3 | import React from 'react' 4 | 5 | const Introduction = () => { 6 | return ( 7 |
8 | 12 | Introduction 13 |
16 | ) 17 | } 18 | 19 | export default Introduction 20 | -------------------------------------------------------------------------------- /docs/src/components/Text.jsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import React from 'react' 3 | 4 | const Text = ({ className }) => { 5 | return ( 6 |
7 | 14 | 15 | 16 | 21 | 26 | 27 | 28 | 33 | 38 | 39 | 40 | 41 | 42 | 49 | 50 | 55 | 60 | 65 | 70 | 71 | 72 |
73 | ) 74 | } 75 | 76 | export default Text 77 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /docs/src/pages/index.jsx: -------------------------------------------------------------------------------- 1 | import Head from '@docusaurus/Head' 2 | import Layout from '@theme/Layout' 3 | import React from 'react' 4 | 5 | import Hero from '../components/Hero' 6 | import Introduction from '../components/Introduction' 7 | import Features from '../components/Features' 8 | 9 | const Home = () => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 |
21 |
22 | ) 23 | } 24 | 25 | export default Home 26 | -------------------------------------------------------------------------------- /docs/static/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dc7290/next-export-optimize-images/ac9f92fe083dcd5788a809042397c59f13a204ca/docs/static/og.png -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | corePlugins: { 8 | preflight: false, 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /image.d.ts: -------------------------------------------------------------------------------- 1 | import Image from './dist/components/client/image' 2 | export * from './dist/components/client/image' 3 | export default Image 4 | -------------------------------------------------------------------------------- /image.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/components/client/image') 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { createDefaultPreset } = require('ts-jest') 2 | 3 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 4 | module.exports = { 5 | ...createDefaultPreset(), 6 | testMatch: ['**/__tests__/**/*.test.[jt]s?(x)'], 7 | } 8 | -------------------------------------------------------------------------------- /legacy/image.d.ts: -------------------------------------------------------------------------------- 1 | import Image from '../dist/components/client/legacy/image' 2 | export default Image 3 | -------------------------------------------------------------------------------- /legacy/image.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../dist/components/client/legacy/image') 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-export-optimize-images", 3 | "version": "4.6.2", 4 | "description": "Optimize images at build time with Next.js.", 5 | "keywords": [ 6 | "next.js", 7 | "static", 8 | "export", 9 | "image", 10 | "optimization" 11 | ], 12 | "homepage": "https://next-export-optimize-images.vercel.app", 13 | "bugs": { 14 | "url": "https://github.com/dc7290/next-export-optimize-images/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/dc7290/next-export-optimize-images.git" 19 | }, 20 | "license": "MIT", 21 | "author": "dc7290 ", 22 | "main": "dist/index.js", 23 | "types": "dist/index.d.ts", 24 | "bin": "bin/index.js", 25 | "files": [ 26 | "dist", 27 | "legacy", 28 | "image.js", 29 | "image.d.ts", 30 | "picture.js", 31 | "picture.d.ts", 32 | "remote-image.js", 33 | "remote-image.d.ts", 34 | "remote-picture.js", 35 | "remote-picture.d.ts" 36 | ], 37 | "scripts": { 38 | "dev": "tsup --watch", 39 | "build": "tsup --dts", 40 | "commitmsg": "commitlint -e $GIT_PARAMS", 41 | "lint": "biome lint ./", 42 | "format": "biome format --write ./", 43 | "check:write": "biome check --write ./", 44 | "lint-staged": "lint-staged", 45 | "prepare": "husky", 46 | "pretest": "npm run build && npm-run-all --sequential pretest:**", 47 | "pretest:e2e": "cd __tests__/e2e && rimraf {.next,out} && next build && node cli.js", 48 | "pretest:e2e-build": "cd __tests__/e2e-build && rimraf .next && next build && node cli.js", 49 | "test": "jest", 50 | "test:watch": "jest --watch", 51 | "semantic-release": "SKIP_BY_SEMANTIC_RELEASE=true semantic-release", 52 | "typecheck": "tsc --noEmit" 53 | }, 54 | "lint-staged": { 55 | "*.{ts,tsx}": [ 56 | "biome check --write" 57 | ] 58 | }, 59 | "dependencies": { 60 | "ansi-colors": "^4.1.3", 61 | "app-root-path": "^3.1.0", 62 | "cli-progress": "^3.12.0", 63 | "fs-extra": "^11.3.0", 64 | "lodash.uniqwith": "^4.5.0", 65 | "recursive-readdir": "^2.2.3", 66 | "sharp": "^0.34.2" 67 | }, 68 | "devDependencies": { 69 | "@biomejs/biome": "1.9.4", 70 | "@commitlint/cli": "19.8.1", 71 | "@commitlint/config-conventional": "19.8.1", 72 | "@semantic-release/changelog": "6.0.3", 73 | "@semantic-release/git": "10.0.1", 74 | "@tsconfig/strictest": "2.0.5", 75 | "@types/app-root-path": "1.2.8", 76 | "@types/cli-progress": "3.11.6", 77 | "@types/fs-extra": "11.0.4", 78 | "@types/jest": "29.5.14", 79 | "@types/lodash.uniqwith": "4.5.9", 80 | "@types/node": "22.15.30", 81 | "@types/react": "npm:types-react@rc", 82 | "@types/recursive-readdir": "2.2.4", 83 | "conventional-changelog-conventionalcommits": "6.1.0", 84 | "git-cz": "4.9.0", 85 | "husky": "9.1.7", 86 | "jest": "29.7.0", 87 | "jest-environment-jsdom": "29.7.0", 88 | "lint-staged": "15.5.2", 89 | "next": "15.3.3", 90 | "npm-run-all2": "7.0.2", 91 | "react": "19.1.0", 92 | "react-dom": "19.1.0", 93 | "rimraf": "6.0.1", 94 | "semantic-release": "19.0.5", 95 | "ts-jest": "29.3.4", 96 | "tsup": "8.5.0", 97 | "typescript": "5.8.3", 98 | "webpack": "5.99.9" 99 | }, 100 | "peerDependencies": { 101 | "next": "14.x || 15.x", 102 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0", 103 | "react-dom": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" 104 | }, 105 | "engines": { 106 | "node": ">=18" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /picture.d.ts: -------------------------------------------------------------------------------- 1 | import Picture from './dist/components/client/picture' 2 | export default Picture 3 | -------------------------------------------------------------------------------- /picture.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/components/client/picture') 2 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A configuration file for semantic-release 3 | * 4 | * @see {@link https://semantic-release.gitbook.io/semantic-release/} for about semantic-release. 5 | * @see {@link https://semantic-release.gitbook.io/semantic-release/usage/configuration} for configuration details. 6 | * @see {@link https://github.com/semantic-release/semantic-release/blob/971a5e0d16f1a32e117e9ce382a1618c8256d0d9/lib/get-config.js#L56} for about default config. 7 | */ 8 | 9 | const types = require('./commit-types.config') 10 | 11 | /** 12 | * GitHubのデフォルトブランチ 13 | */ 14 | const defaultBranch = 'release' 15 | 16 | /** 17 | * changelogを書き出すファイル名 18 | */ 19 | const changelogFile = 'CHANGELOG.md' 20 | 21 | module.exports = { 22 | /** 23 | * リリース対象となるGitブランチ 24 | * 25 | * @see https://semantic-release.gitbook.io/semantic-release/usage/workflow-configuration 26 | */ 27 | branches: [defaultBranch, { name: 'beta', prerelease: true }], 28 | /** 29 | * Gitタグのフォーマット。Lodashのテンプレートが使えます。 30 | * multi-semantic-releaseを使った場合は、この設定は無視されます。 31 | */ 32 | tagFormat: 'v${version}', 33 | /** 34 | * 実行するプラグイン 35 | */ 36 | plugins: [ 37 | /** 38 | * conventional-changelogでコミットを解析します。 39 | * @see https://github.com/semantic-release/commit-analyzer 40 | */ 41 | [ 42 | '@semantic-release/commit-analyzer', 43 | { 44 | preset: 'conventionalcommits', 45 | releaseRules: [ 46 | { breaking: true, release: 'major' }, 47 | { revert: true, release: 'patch' }, 48 | ...types.flatMap(({ type, release }) => (release ? [{ type, release }] : [])), 49 | ], 50 | }, 51 | ], 52 | /** 53 | * conventional-changelogでchangelogコンテンツを生成します。 54 | * @see https://github.com/semantic-release/release-notes-generator 55 | */ 56 | [ 57 | '@semantic-release/release-notes-generator', 58 | { 59 | preset: 'conventionalcommits', 60 | presetConfig: { 61 | types: types.map(({ type, section, hidden }) => ({ 62 | type, 63 | section, 64 | hidden: hidden ?? true, 65 | })), 66 | }, 67 | }, 68 | ], 69 | /** 70 | * changelogコンテンツをもとにchangelogFileを生成します。 71 | * @see https://github.com/semantic-release/changelog 72 | */ 73 | [ 74 | '@semantic-release/changelog', 75 | { 76 | changelogFile, 77 | changelogTitle: 78 | '# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/).\n\n## [Released](https://github.com/dc7290/next-export-optimize-images/releases)', 79 | }, 80 | ], 81 | /** 82 | * package.jsonのバージョンを更新したり、npmパッケージを公開します。 83 | * @see https://github.com/semantic-release/npm 84 | */ 85 | [ 86 | '@semantic-release/npm', 87 | { 88 | // npmに公開するかどうか 89 | npmPublish: true, 90 | }, 91 | ], 92 | /** 93 | * リリース時に生成したアセットをGitリポジトリにコミットします。 94 | * @see https://github.com/semantic-release/git 95 | */ 96 | [ 97 | '@semantic-release/git', 98 | { 99 | // コミット対象のファイル 100 | assets: [ 101 | 'package.json', // versionフィールドの変更をコミットするため 102 | 'yarn.lock', // versionフィールドの変更をコミットするため 103 | changelogFile, // changelogFileの変更をコミットするため 104 | ], 105 | // コミットメッセージ 106 | message: 'release: 🏹 ${nextRelease.gitTag} [skip ci]\n\n${nextRelease.notes}', 107 | }, 108 | ], 109 | /** 110 | * GitHub releaseを公開し、リリースされたプルリクエストやissueにコメントを残します。assetsをreleasesにアップロードすることもできます。 111 | * @see https://github.com/semantic-release/github 112 | */ 113 | [ 114 | '@semantic-release/github', 115 | { 116 | // 関連するissueやPRにつけるラベル 117 | releasedLabels: ['released', 'released-in-${nextRelease.gitTag}'], 118 | // 関連するissueやPRに残すコメント 119 | successComment: 120 | "🎉 This ${issue.pull_request ? 'pull request' : 'issue'} is included in version ${nextRelease.gitTag}.", 121 | }, 122 | ], 123 | ], 124 | } 125 | -------------------------------------------------------------------------------- /remote-image.d.ts: -------------------------------------------------------------------------------- 1 | import RemoteImage from './dist/components/server/remote-image' 2 | export * from './dist/components/server/remote-image' 3 | export default RemoteImage 4 | -------------------------------------------------------------------------------- /remote-image.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/components/server/remote-image') 2 | -------------------------------------------------------------------------------- /remote-picture.d.ts: -------------------------------------------------------------------------------- 1 | import RemotePicture from './dist/components/server/remote-picture' 2 | export * from './dist/components/server/remote-picture' 3 | export default RemotePicture 4 | -------------------------------------------------------------------------------- /remote-picture.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/components/server/remote-picture') 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", ":timezone(Asia/Tokyo)"], 3 | "schedule": ["after 2am and before 10am every weekend"], 4 | "npm": { 5 | "rangeStrategy": "bump" 6 | }, 7 | "packageRules": [ 8 | { 9 | "matchUpdateTypes": ["minor", "patch"], 10 | "automerge": true 11 | }, 12 | { 13 | "depTypeList": ["peerDependencies", "engines"], 14 | "enabled": false 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/cli/external-images/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { Stream } from 'node:stream' 3 | import type { ReadableStream } from 'node:stream/web' 4 | import fs from 'fs-extra' 5 | import type { Manifest } from '../' 6 | import type { Config } from './../../utils/getConfig' 7 | 8 | type ExternalImagesDownloaderArgs = { 9 | terse?: boolean 10 | manifest: Manifest 11 | destDir: string 12 | remoteImagesDownloadsDelay?: Config['remoteImagesDownloadsDelay'] 13 | } 14 | 15 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) 16 | 17 | const externalImagesDownloader = async ({ 18 | terse = false, 19 | manifest, 20 | destDir, 21 | remoteImagesDownloadsDelay, 22 | }: ExternalImagesDownloaderArgs) => { 23 | if (!terse) { 24 | // eslint-disable-next-line no-console 25 | console.log('\n- Download external images -') 26 | } 27 | 28 | const promises: Promise[] = [] 29 | const downloadedImages: string[] = [] 30 | 31 | for (const { src, externalUrl } of manifest) { 32 | if (externalUrl === undefined) continue 33 | 34 | if (downloadedImages.some((image) => image === externalUrl)) continue 35 | 36 | if (remoteImagesDownloadsDelay) { 37 | await sleep(remoteImagesDownloadsDelay) 38 | } 39 | 40 | promises.push( 41 | (async (): Promise => { 42 | downloadedImages.push(externalUrl) 43 | 44 | const outputPath = path.join(destDir, src) 45 | await fs.ensureFile(outputPath) 46 | 47 | const body = await fetch(externalUrl) 48 | .then((response) => response.body) 49 | .catch((e) => { 50 | throw new Error(`Failed to download \`${externalUrl}\`: ${e}`) 51 | }) 52 | 53 | if (body === null) { 54 | throw new Error(`Failed to download \`${externalUrl}\`: reason: body is null`) 55 | } 56 | 57 | const readableNodeStream = Stream.Readable.fromWeb(body as ReadableStream) 58 | const fileStream = fs.createWriteStream(outputPath) 59 | 60 | return new Promise((resolve, reject) => { 61 | readableNodeStream.pipe(fileStream) 62 | readableNodeStream.on('error', reject) 63 | fileStream.on('finish', () => { 64 | if (!terse) { 65 | // eslint-disable-next-line no-console 66 | console.log(`\`${externalUrl}\` has been downloaded.`) 67 | } 68 | resolve() 69 | }) 70 | }) 71 | })() 72 | ) 73 | } 74 | 75 | await Promise.all(promises) 76 | } 77 | 78 | export default externalImagesDownloader 79 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'node:crypto' 2 | import path from 'node:path' 3 | import colors from 'ansi-colors' 4 | import fs from 'fs-extra' 5 | import { PHASE_PRODUCTION_BUILD } from 'next/constants' 6 | import loadConfig from 'next/dist/server/config' 7 | import type { ImageConfigComplete } from 'next/dist/shared/lib/image-config' 8 | import recursiveReadDir from 'recursive-readdir' 9 | import sharp from 'sharp' 10 | import buildOutputInfo from '../utils/buildOutputInfo' 11 | import formatValidate from '../utils/formatValidate' 12 | import getConfig, { type Config } from '../utils/getConfig' 13 | import processManifest from '../utils/processManifest' 14 | import externalImagesDownloader from './external-images' 15 | import { type CacheImages, createCacheDir, defaultCacheDir, readCacheManifest, writeCacheManifest } from './utils/cache' 16 | import { cliProgressBarIncrement, cliProgressBarStart } from './utils/cliProgressBar' 17 | import uniqueItems from './utils/uniqueItems' 18 | 19 | export type Manifest = { 20 | output: string 21 | src: string 22 | width: number 23 | extension: string 24 | externalUrl?: string 25 | }[] 26 | 27 | type GetOptimizeResultProps = { 28 | imageBuffer: Buffer 29 | destDir: string 30 | noCache: boolean 31 | cacheImages: CacheImages 32 | cacheDir: string 33 | cacheMeasurement: () => void 34 | nonCacheMeasurement: () => void 35 | errorMeasurement: () => void 36 | pushInvalidFormatAssets: (asset: string) => void 37 | cliProgressBarIncrement: () => void 38 | originalFilePath: string 39 | quality: number 40 | sharpOptions?: Config['sharpOptions'] 41 | } & Omit 42 | type GetOptimizeResult = (getOptimizeResultProps: GetOptimizeResultProps) => Promise 43 | 44 | export const getOptimizeResult: GetOptimizeResult = async ({ 45 | imageBuffer, 46 | destDir, 47 | noCache, 48 | cacheImages, 49 | cacheDir, 50 | cacheMeasurement, 51 | nonCacheMeasurement, 52 | errorMeasurement, 53 | pushInvalidFormatAssets, 54 | cliProgressBarIncrement, 55 | originalFilePath, 56 | quality, 57 | sharpOptions, 58 | output, 59 | width, 60 | extension, 61 | }) => { 62 | if (formatValidate(extension)) { 63 | try { 64 | const filePath = path.join(destDir, output) 65 | await fs.ensureFile(filePath) 66 | 67 | const outputPath = path.join(cacheDir, output) 68 | await fs.ensureFile(outputPath) 69 | 70 | // Cache process 71 | if (!noCache) { 72 | const cacheImagesFindIndex = cacheImages.findIndex((cacheImage) => cacheImage.output === output) 73 | const hash = createHash('sha256').update(imageBuffer).digest('hex') 74 | 75 | if (cacheImagesFindIndex === -1) { 76 | cacheImages.push({ output, hash }) 77 | } else { 78 | const currentCacheImage = cacheImages[cacheImagesFindIndex] 79 | if (currentCacheImage?.hash === hash) { 80 | await fs.copy(outputPath, filePath) 81 | cacheMeasurement() 82 | cliProgressBarIncrement() 83 | return 84 | } 85 | if (currentCacheImage !== undefined) { 86 | currentCacheImage.hash = hash 87 | } 88 | } 89 | } 90 | 91 | const image = sharp(imageBuffer, { sequentialRead: true, animated: true }) 92 | 93 | image.rotate().resize({ width, withoutEnlargement: true }) 94 | 95 | switch (extension) { 96 | case 'jpeg': { 97 | const jpeg = await image.jpeg({ quality, ...sharpOptions?.jpg }) 98 | await jpeg.toFile(outputPath) 99 | await jpeg.toFile(filePath) 100 | break 101 | } 102 | case 'jpg': { 103 | const jpg = await image.jpeg({ quality, ...sharpOptions?.jpg }) 104 | await jpg.toFile(outputPath) 105 | await jpg.toFile(filePath) 106 | break 107 | } 108 | case 'png': { 109 | const png = await image.png({ quality, ...sharpOptions?.png }) 110 | await png.toFile(outputPath) 111 | await png.toFile(filePath) 112 | break 113 | } 114 | case 'webp': { 115 | const webp = image.webp({ quality, ...sharpOptions?.webp }) 116 | await webp.toFile(outputPath) 117 | await webp.toFile(filePath) 118 | break 119 | } 120 | case 'avif': { 121 | const avif = image.avif({ quality, ...sharpOptions?.avif }) 122 | await avif.toFile(outputPath) 123 | await avif.toFile(filePath) 124 | break 125 | } 126 | } 127 | 128 | nonCacheMeasurement() 129 | } catch (error) { 130 | console.warn(error) 131 | errorMeasurement() 132 | } finally { 133 | cliProgressBarIncrement() 134 | } 135 | } else { 136 | try { 137 | const filePath = path.join(destDir, output) 138 | await fs.ensureFile(filePath) 139 | 140 | await fs.copy(originalFilePath, filePath) 141 | 142 | extension !== 'svg' && pushInvalidFormatAssets(originalFilePath) 143 | } catch (error) { 144 | console.warn(error) 145 | errorMeasurement() 146 | } finally { 147 | cliProgressBarIncrement() 148 | } 149 | } 150 | } 151 | 152 | const cwd = process.cwd() 153 | 154 | type OptimizeImagesProps = { 155 | manifestJsonPath: string 156 | noCache: boolean 157 | config: Config 158 | nextImageConfig: ImageConfigComplete 159 | terse?: boolean 160 | } 161 | 162 | export const optimizeImages = async ({ 163 | manifestJsonPath, 164 | noCache, 165 | config, 166 | nextImageConfig, 167 | terse = false, 168 | }: OptimizeImagesProps) => { 169 | const destDir = config.mode === 'build' ? cwd : path.resolve(cwd, config.outDir ?? 'out') 170 | const srcDir = config.mode === 'build' ? cwd : destDir 171 | 172 | let manifest: Manifest = [] 173 | try { 174 | if (fs.existsSync(manifestJsonPath)) { 175 | manifest = uniqueItems(processManifest(await fs.readFile(manifestJsonPath, 'utf-8'))) 176 | } 177 | } catch (error) { 178 | throw Error(typeof error === 'string' ? error : 'Unexpected error.') 179 | } 180 | 181 | // Next Image allSizes 182 | const allSizes = [...nextImageConfig.imageSizes, ...nextImageConfig.deviceSizes] 183 | 184 | // External image if present 185 | const remoteImages = 186 | config.remoteImages === undefined 187 | ? [] 188 | : typeof config.remoteImages === 'function' 189 | ? await config.remoteImages() 190 | : config.remoteImages 191 | if (remoteImages.length > 0) { 192 | const remoteImageList = new Set() 193 | 194 | for (const url of remoteImages) { 195 | remoteImageList.add(url) 196 | } 197 | 198 | manifest = manifest.concat( 199 | Array.from(remoteImageList) 200 | .map((url) => 201 | allSizes.map((size) => { 202 | return buildOutputInfo({ 203 | src: url, 204 | width: size, 205 | config, 206 | }).map(({ output, extension, originalExtension }) => { 207 | const externalOutputDir = `${ 208 | config.externalImageDir 209 | ? config.externalImageDir.replace(/^\//, '').replace(/\/$/, '') 210 | : '_next/static/media' 211 | }` 212 | 213 | const json: Manifest[number] = { 214 | output, 215 | src: `/${config.mode === 'build' ? externalOutputDir.replace(/^_next/, '.next') : externalOutputDir}/${createHash( 216 | 'sha256' 217 | ) 218 | .update( 219 | url 220 | .replace(/^https?:\/\//, '') 221 | .split('/') 222 | .slice(1) 223 | .join('/') 224 | ) 225 | .digest('hex')}.${originalExtension}`, 226 | width: size, 227 | extension, 228 | externalUrl: url, 229 | } 230 | 231 | return json 232 | }) 233 | }) 234 | ) 235 | .flat(2) 236 | ) 237 | } 238 | if (manifest.some(({ externalUrl }) => externalUrl !== undefined)) { 239 | await externalImagesDownloader({ 240 | terse, 241 | manifest, 242 | destDir, 243 | remoteImagesDownloadsDelay: config.remoteImagesDownloadsDelay, 244 | }) 245 | } 246 | 247 | const publicDir = path.resolve(cwd, 'public') 248 | if (fs.existsSync(publicDir)) { 249 | if (!terse) { 250 | console.log('\n- Collect images in public directory -') 251 | } 252 | const ignorePaths = config.ignorePaths ? config.ignorePaths.map((p) => path.join(publicDir, p)) : [] 253 | 254 | const publicDirFiles = await recursiveReadDir(publicDir) 255 | const publicDirImages = publicDirFiles 256 | .filter((file) => { 257 | const ext = path.extname(file).toLowerCase() 258 | return ( 259 | ext === '.png' || ext === '.jpg' || ext === '.jpeg' || ext === '.webp' || ext === '.avif' || ext === '.gif' 260 | ) 261 | }) 262 | .filter((file) => !ignorePaths.includes(file)) 263 | manifest = manifest.concat( 264 | publicDirImages 265 | .map((file) => 266 | allSizes.map((size) => { 267 | const ignorePath = config.mode === 'build' ? publicDir.replace(/public$/, '') : publicDir // The public directory in the root is required when building. 268 | const src = file.replace(ignorePath, '') 269 | return buildOutputInfo({ 270 | src, 271 | width: size, 272 | config, 273 | }).map(({ output, extension }) => { 274 | const json: Manifest[number] = { 275 | output, 276 | src, 277 | width: size, 278 | extension, 279 | } 280 | 281 | return json 282 | }) 283 | }) 284 | ) 285 | .flat(2) 286 | ) 287 | } 288 | 289 | if (!terse) { 290 | console.log('\n- Image Optimization -') 291 | cliProgressBarStart(manifest.length) 292 | } 293 | 294 | let cacheImages: CacheImages = [] 295 | 296 | if (!noCache) { 297 | await createCacheDir() 298 | cacheImages = readCacheManifest() 299 | } 300 | 301 | let measuredCache = 0 302 | let measuredNonCache = 0 303 | let measuredError = 0 304 | const invalidFormatAssets = new Set([]) 305 | 306 | const cacheMeasurement = () => { 307 | measuredCache += 1 308 | } 309 | const nonCacheMeasurement = () => { 310 | measuredNonCache += 1 311 | } 312 | const errorMeasurement = () => { 313 | measuredError += 1 314 | } 315 | const pushInvalidFormatAssets = (asset: string) => invalidFormatAssets.add(asset) 316 | 317 | const srcMap: Record[]> = {} 318 | for (const item of manifest) { 319 | const { src, ...rest } = item 320 | if (src in srcMap) { 321 | srcMap[src]?.push(rest) 322 | } else { 323 | srcMap[src] = [rest] 324 | } 325 | } 326 | 327 | const promises: Promise[] = [] 328 | 329 | for (const key in srcMap) { 330 | const items = srcMap[key] 331 | 332 | if (items === undefined || items.length === 0) continue 333 | 334 | const originalFilePath = path.join(srcDir, config.mode === 'build' ? key.replace(/^\/_next/, '/.next') : key) 335 | const imageBuffer = await fs.readFile(originalFilePath) 336 | 337 | for (const item of items) { 338 | item.output = config.mode === 'build' ? item.output.replace(/^\/_next/, '/.next') : item.output 339 | 340 | promises.push( 341 | getOptimizeResult({ 342 | imageBuffer, 343 | destDir, 344 | noCache, 345 | cacheImages, 346 | cacheDir: defaultCacheDir, 347 | cacheMeasurement, 348 | nonCacheMeasurement, 349 | errorMeasurement, 350 | pushInvalidFormatAssets, 351 | cliProgressBarIncrement: terse ? () => undefined : cliProgressBarIncrement, 352 | originalFilePath, 353 | quality: config.quality ?? 75, 354 | sharpOptions: config.sharpOptions ?? {}, 355 | ...item, 356 | }) 357 | ) 358 | } 359 | } 360 | 361 | await Promise.all(promises) 362 | 363 | if (!noCache) { 364 | writeCacheManifest(cacheImages) 365 | } 366 | 367 | if (!terse) { 368 | // eslint-disable-next-line no-console 369 | console.log(`Cache assets: ${measuredCache}, NonCache assets: ${measuredNonCache}, Error assets: ${measuredError}`) 370 | 371 | if (invalidFormatAssets.size !== 0) { 372 | // eslint-disable-next-line no-console 373 | console.log( 374 | '\nThe following images are in a non-optimized format and a simple copy was applied.\n', 375 | Array.from(invalidFormatAssets).join('\n') 376 | ) 377 | } 378 | 379 | console.log(colors.bold.magenta('\nSuccessful optimization!')) 380 | } 381 | } 382 | 383 | type Run = (args: { noCache?: boolean }) => void 384 | 385 | export const run: Run = async ({ noCache = false }) => { 386 | // eslint-disable-next-line no-console 387 | console.log(colors.bold.magenta('\nnext-export-optimize-images: Optimize images.')) 388 | 389 | const config = getConfig() 390 | const manifestJsonPath = path.resolve(cwd, '.next/next-export-optimize-images-list.nd.json') 391 | 392 | const nextConfig = await loadConfig(PHASE_PRODUCTION_BUILD, cwd) 393 | 394 | await optimizeImages({ manifestJsonPath, noCache, config, nextImageConfig: nextConfig.images }) 395 | } 396 | -------------------------------------------------------------------------------- /src/cli/utils/cache.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | import fs from 'fs-extra' 4 | 5 | import getConfig from '../../utils/getConfig' 6 | 7 | const cacheDir = getConfig().cacheDir || 'node_modules/.cache/next-export-optimize-images' 8 | 9 | export const defaultCacheDir = cacheDir.startsWith('/') ? cacheDir : path.join(process.cwd(), cacheDir) 10 | export const defaultCacheFilePath = path.join(defaultCacheDir, 'cached-images.json') 11 | 12 | export const createCacheDir = async (cacheDir = defaultCacheDir) => { 13 | await fs.mkdirp(cacheDir) 14 | } 15 | 16 | export type CacheImages = { 17 | output: string 18 | hash: string 19 | }[] 20 | 21 | export const readCacheManifest = (filePath = defaultCacheFilePath): CacheImages => { 22 | try { 23 | return JSON.parse(fs.readFileSync(filePath, 'utf-8')) 24 | } catch (_) { 25 | return [] 26 | } 27 | } 28 | 29 | export const writeCacheManifest = (cacheImages: CacheImages, filePath = defaultCacheFilePath) => { 30 | fs.writeFileSync(filePath, JSON.stringify(cacheImages), 'utf-8') 31 | } 32 | -------------------------------------------------------------------------------- /src/cli/utils/cliProgressBar.ts: -------------------------------------------------------------------------------- 1 | import colors from 'ansi-colors' 2 | import cliProgress from 'cli-progress' 3 | 4 | const bar = new cliProgress.SingleBar({ 5 | format: `Optimization progress |${colors.cyan('{bar}')}| {percentage}% || {value}/{total} Chunks`, 6 | barCompleteChar: '\u2588', 7 | barIncompleteChar: '\u2591', 8 | hideCursor: true, 9 | stopOnComplete: true, 10 | }) 11 | 12 | export const cliProgressBarStart = (total: number) => { 13 | bar.start(total, 0) 14 | } 15 | 16 | export const cliProgressBarIncrement = (addNumber?: number) => { 17 | bar.increment(addNumber) 18 | } 19 | -------------------------------------------------------------------------------- /src/cli/utils/uniqueItems.ts: -------------------------------------------------------------------------------- 1 | import uniqWith from 'lodash.uniqwith' 2 | 3 | import type { Manifest } from '../' 4 | 5 | const uniqueItems = (items: Manifest) => 6 | uniqWith(items, (a, b) => a.output === b.output && a.width === b.width && a.extension === b.extension) 7 | 8 | export default uniqueItems 9 | -------------------------------------------------------------------------------- /src/components/client/image.tsx: -------------------------------------------------------------------------------- 1 | import Image, { type ImageProps } from 'next/image' 2 | import React, { forwardRef } from 'react' 3 | import getStringSrc from '../utils/getStringSrc' 4 | import imageLoader from '../utils/imageLoader' 5 | 6 | const CustomImage = forwardRef((props, forwardedRef) => { 7 | const srcStr = getStringSrc(props.src) 8 | const blurDataURLObj = props.blurDataURL 9 | ? { blurDataURL: props.blurDataURL } 10 | : typeof props.src === 'string' && props.placeholder === 'blur' && props.loader === undefined 11 | ? { blurDataURL: imageLoader()({ src: props.src, width: 8, quality: 10 }) } 12 | : {} 13 | 14 | return ( 15 | 22 | ) 23 | }) 24 | CustomImage.displayName = 'CustomImage' 25 | 26 | export { default as getOptimizedImageProps } from '../utils/getOptimizedImageProps' 27 | export default CustomImage 28 | -------------------------------------------------------------------------------- /src/components/client/legacy/image.tsx: -------------------------------------------------------------------------------- 1 | import Image, { type ImageProps } from 'next/image' 2 | import React from 'react' 3 | import imageLoader from '../../utils/imageLoader' 4 | 5 | const CustomImage = (props: ImageProps) => { 6 | return ( 7 | 17 | ) 18 | } 19 | 20 | export default CustomImage 21 | -------------------------------------------------------------------------------- /src/components/client/picture.tsx: -------------------------------------------------------------------------------- 1 | import Image, { type ImageProps, getImageProps } from 'next/image' 2 | import React, { forwardRef } from 'react' 3 | import getConfig from '../../utils/getConfig' 4 | import getStringSrc from '../utils/getStringSrc' 5 | import imageLoader from '../utils/imageLoader' 6 | 7 | const config = getConfig() 8 | 9 | const Picture = forwardRef((props, forwardedRef) => { 10 | const srcStr = getStringSrc(props.src) 11 | 12 | if (srcStr.endsWith('.svg')) { 13 | return 14 | } 15 | 16 | const blurDataURLObj = props.blurDataURL 17 | ? { blurDataURL: props.blurDataURL } 18 | : typeof props.src === 'string' && props.placeholder === 'blur' && props.loader === undefined 19 | ? { blurDataURL: imageLoader()({ src: props.src, width: 8, quality: 10 }) } 20 | : {} 21 | 22 | const additionalFormats = [...new Set(config.generateFormats ?? ['webp'])] 23 | const sources = additionalFormats.map((format, i) => { 24 | const sourceProps = getImageProps({ 25 | ...props, 26 | loader: imageLoader(i), 27 | }).props 28 | return { 29 | srcSet: sourceProps.srcSet, 30 | type: `image/${format}`, 31 | width: sourceProps.width, 32 | height: sourceProps.height, 33 | sizes: sourceProps.sizes, 34 | } 35 | }) 36 | 37 | return ( 38 | 39 | {sources.map((source) => ( 40 | 41 | ))} 42 | 43 | 44 | ) 45 | }) 46 | Picture.displayName = 'Picture' 47 | 48 | export default Picture 49 | -------------------------------------------------------------------------------- /src/components/server/remote-image.tsx: -------------------------------------------------------------------------------- 1 | import { createHash } from 'node:crypto' 2 | import { join } from 'node:path' 3 | import { appendFileSync } from 'fs-extra' 4 | import type { ImageConfigComplete } from 'next/dist/shared/lib/image-config' 5 | import type { ImageProps } from 'next/image' 6 | import React, { forwardRef } from 'react' 7 | import type { Manifest } from '../../cli' 8 | import buildOutputInfo from '../../utils/buildOutputInfo' 9 | import getConfig from '../../utils/getConfig' 10 | import Image from '../client/image' 11 | 12 | type RemoteImageProps = Omit & { 13 | src: string 14 | } 15 | 16 | const config = getConfig() 17 | 18 | const RemoteImage = forwardRef(({ src, ...props }, forwardedRef) => { 19 | if (process.env.NODE_ENV === 'production') { 20 | const nextImageConfig = process.env.__NEXT_IMAGE_OPTS as unknown as ImageConfigComplete 21 | 22 | const allSizes = [...nextImageConfig.imageSizes, ...nextImageConfig.deviceSizes] 23 | 24 | for (const width of allSizes) { 25 | const outputInfo = buildOutputInfo({ 26 | src, 27 | width, 28 | config, 29 | }).at(-1) 30 | 31 | if (outputInfo === undefined) { 32 | throw new Error(`No output info found for ${src}`) 33 | } 34 | 35 | const { output, extension, originalExtension } = outputInfo 36 | 37 | const externalOutputDir = `${ 38 | config.externalImageDir ? config.externalImageDir.replace(/^\//, '').replace(/\/$/, '') : '_next/static/media' 39 | }` 40 | 41 | const json: Manifest[number] = { 42 | output, 43 | src: `/${config.mode === 'build' ? externalOutputDir.replace(/^_next/, '.next') : externalOutputDir}/${createHash( 44 | 'sha256' 45 | ) 46 | .update( 47 | src 48 | .replace(/^https?:\/\//, '') 49 | .split('/') 50 | .slice(1) 51 | .join('/') 52 | ) 53 | .digest('hex')}.${originalExtension}`, 54 | width, 55 | extension, 56 | externalUrl: src, 57 | } 58 | 59 | appendFileSync(join(process.cwd(), '.next/next-export-optimize-images-list.nd.json'), `${JSON.stringify(json)}\n`) 60 | } 61 | } 62 | 63 | return 64 | }) 65 | RemoteImage.displayName = 'RemoteImage' 66 | 67 | export default RemoteImage 68 | -------------------------------------------------------------------------------- /src/components/server/remote-picture.tsx: -------------------------------------------------------------------------------- 1 | import { createHash } from 'node:crypto' 2 | import { join } from 'node:path' 3 | import { appendFileSync } from 'fs-extra' 4 | import type { ImageConfigComplete } from 'next/dist/shared/lib/image-config' 5 | import type { ImageProps } from 'next/image' 6 | import React, { forwardRef } from 'react' 7 | import type { Manifest } from '../../cli' 8 | import buildOutputInfo from '../../utils/buildOutputInfo' 9 | import getConfig from '../../utils/getConfig' 10 | import Picture from '../client/picture' 11 | 12 | type RemotePictureProps = Omit & { 13 | src: string 14 | } 15 | 16 | const config = getConfig() 17 | 18 | const RemotePicture = forwardRef(({ src, ...props }, forwardedRef) => { 19 | if (process.env.NODE_ENV === 'production') { 20 | const nextImageConfig = process.env.__NEXT_IMAGE_OPTS as unknown as ImageConfigComplete 21 | 22 | const allSizes = [...nextImageConfig.imageSizes, ...nextImageConfig.deviceSizes] 23 | 24 | for (const width of allSizes) { 25 | const outputInfo = buildOutputInfo({ 26 | src, 27 | width, 28 | config, 29 | }) 30 | for (const { output, extension, originalExtension } of outputInfo) { 31 | const externalOutputDir = `${ 32 | config.externalImageDir ? config.externalImageDir.replace(/^\//, '').replace(/\/$/, '') : '_next/static/media' 33 | }` 34 | 35 | const json: Manifest[number] = { 36 | output, 37 | src: `/${config.mode === 'build' ? externalOutputDir.replace(/^_next/, '.next') : externalOutputDir}/${createHash( 38 | 'sha256' 39 | ) 40 | .update( 41 | src 42 | .replace(/^https?:\/\//, '') 43 | .split('/') 44 | .slice(1) 45 | .join('/') 46 | ) 47 | .digest('hex')}.${originalExtension}`, 48 | width, 49 | extension, 50 | externalUrl: src, 51 | } 52 | 53 | appendFileSync( 54 | join(process.cwd(), '.next/next-export-optimize-images-list.nd.json'), 55 | `${JSON.stringify(json)}\n` 56 | ) 57 | } 58 | } 59 | } 60 | 61 | return 62 | }) 63 | RemotePicture.displayName = 'RemotePicture' 64 | 65 | export default RemotePicture 66 | -------------------------------------------------------------------------------- /src/components/utils/getOptimizedImageProps.ts: -------------------------------------------------------------------------------- 1 | import { type ImageProps, getImageProps } from 'next/image' 2 | import getStringSrc from './getStringSrc' 3 | import imageLoader from './imageLoader' 4 | 5 | export type ImgProps = ReturnType 6 | 7 | const getOptimizedImageProps = (props: ImageProps): ImgProps => { 8 | const srcStr = getStringSrc(props.src) 9 | 10 | return getImageProps({ 11 | ...props, 12 | loader: props.loader || imageLoader(), 13 | ...(props.blurDataURL 14 | ? { blurDataURL: props.blurDataURL } 15 | : typeof props.src === 'string' && props.placeholder === 'blur' && props.loader === undefined 16 | ? { blurDataURL: imageLoader()({ src: props.src, width: 8, quality: 10 }) } 17 | : {}), 18 | unoptimized: props.unoptimized !== undefined ? props.unoptimized : srcStr.endsWith('.svg'), 19 | }) 20 | } 21 | 22 | export default getOptimizedImageProps 23 | -------------------------------------------------------------------------------- /src/components/utils/getStringSrc.ts: -------------------------------------------------------------------------------- 1 | import type { ImageProps, StaticImageData } from 'next/dist/shared/lib/image-external' 2 | 3 | type StaticRequire = { 4 | default: StaticImageData 5 | } 6 | 7 | const getStringSrc = (imgSrc: ImageProps['src']) => { 8 | return typeof imgSrc === 'string' 9 | ? imgSrc 10 | : (imgSrc as StaticRequire).default !== undefined 11 | ? (imgSrc as StaticRequire).default.src 12 | : (imgSrc as StaticImageData).src 13 | } 14 | 15 | export default getStringSrc 16 | -------------------------------------------------------------------------------- /src/components/utils/imageLoader.ts: -------------------------------------------------------------------------------- 1 | import type { ImageLoaderProps } from 'next/dist/shared/lib/image-external' 2 | import buildOutputInfo from '../../utils/buildOutputInfo' 3 | import getConfig from '../../utils/getConfig' 4 | 5 | const config = getConfig() 6 | 7 | const imageLoader = 8 | (getNumber?: number) => 9 | ({ src, width }: ImageLoaderProps) => { 10 | if (process.env.NODE_ENV === 'development') { 11 | // This doesn't bother optimizing in the dev environment. Next complains if the 12 | // returned URL doesn't have a width in it, so adding it as a throwaway 13 | return `${src}?width=${width}` 14 | } 15 | 16 | const outputInfo = buildOutputInfo({ src, width, config }).at(getNumber ?? -1) 17 | 18 | if (outputInfo === undefined) { 19 | throw new Error(`No output info found for ${src}`) 20 | } 21 | 22 | return `${config.basePath ?? ''}${outputInfo.output}` 23 | } 24 | 25 | export default imageLoader 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import withExportImages from './withExportImages' 2 | 3 | export type { Config } from './utils/getConfig' 4 | 5 | export default withExportImages 6 | module.exports = withExportImages 7 | -------------------------------------------------------------------------------- /src/loader/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import fs from 'fs-extra' 3 | import { PHASE_PRODUCTION_BUILD } from 'next/constants' 4 | import loadConfig from 'next/dist/server/config' 5 | import type { StaticImageData } from 'next/image' 6 | import getConfig from 'src/utils/getConfig' 7 | import type { LoaderContext } from 'webpack' 8 | import type { Manifest } from '../cli' 9 | import buildOutputInfo from '../utils/buildOutputInfo' 10 | 11 | type LoaderOptions = { 12 | dir: string 13 | isServer: boolean 14 | isDev: boolean 15 | } 16 | 17 | export default async function loader(this: LoaderContext, content: string) { 18 | this.cacheable?.(false) 19 | const callback = this.async() 20 | 21 | const { dir, isDev } = this.getOptions() 22 | 23 | if (isDev) { 24 | callback(null, content) 25 | return 26 | } 27 | 28 | const { src } = JSON.parse(content.replace(/^export default /, '').replace(/;$/, '')) as StaticImageData 29 | 30 | const config = getConfig() 31 | 32 | const nextConfig = await loadConfig(PHASE_PRODUCTION_BUILD, dir) 33 | const allSizes = [...nextConfig.images.deviceSizes, ...nextConfig.images.imageSizes] 34 | 35 | if (!src.endsWith('.svg')) { 36 | await Promise.all( 37 | allSizes.map(async (size) => { 38 | const outputInfo = buildOutputInfo({ src, width: size, config }) 39 | for (const { output, src, extension } of outputInfo) { 40 | const json: Manifest[number] = { 41 | output: output, 42 | src: src, 43 | width: size, 44 | extension: extension, 45 | } 46 | 47 | await fs.appendFile( 48 | path.join(process.cwd(), '.next/next-export-optimize-images-list.nd.json'), 49 | `${JSON.stringify(json)}\n` 50 | ) 51 | } 52 | }) 53 | ) 54 | } 55 | 56 | callback(null, content) 57 | return 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/buildOutputInfo.ts: -------------------------------------------------------------------------------- 1 | import formatValidate from './formatValidate' 2 | import type { Config, DefaultImageParser } from './getConfig' 3 | 4 | export const defaultImageParser: DefaultImageParser = (src: string) => { 5 | const path = src.split(/\.([^.]*$)/)[0] 6 | const extension = (src.split(/\.([^.]*$)/)[1] || '').split('?')[0] 7 | 8 | if (!path || !extension) { 9 | throw new Error(`Invalid path or no file extension: ${src}`) 10 | } 11 | 12 | let pathWithoutName = path.split('/').slice(0, -1).join('/') 13 | const name = path.split('/').slice(-1).toString() 14 | 15 | if (src.startsWith('http')) { 16 | pathWithoutName = pathWithoutName 17 | .replace(/^https?:\/\//, '') 18 | .split('/') 19 | .slice(1) 20 | .join('/') 21 | } 22 | 23 | return { 24 | pathWithoutName, 25 | name, 26 | extension, 27 | } 28 | } 29 | 30 | type BuildOutputInfoArgs = { 31 | src: string 32 | width: number 33 | config: Config 34 | } 35 | 36 | const buildOutputInfo = ({ src: _src, width, config }: BuildOutputInfoArgs) => { 37 | let src = _src 38 | 39 | if (config.basePath !== undefined) { 40 | src = _src.replace(config.basePath, '') 41 | } 42 | 43 | const parsedImageInformation = config.sourceImageParser 44 | ? config.sourceImageParser({ src, defaultParser: defaultImageParser }) 45 | : defaultImageParser(src) 46 | 47 | let { extension } = parsedImageInformation 48 | const { pathWithoutName, name, extension: originalExtension } = parsedImageInformation 49 | 50 | if (config.convertFormat !== undefined) { 51 | const convertArray = config.convertFormat.find(([beforeConvert]) => beforeConvert === extension) 52 | if (convertArray !== undefined) { 53 | if (!formatValidate(convertArray[0])) 54 | throw Error(`Unauthorized format specified in \`configFormat\`. beforeConvert: ${convertArray[0]}`) 55 | if (!formatValidate(convertArray[1])) 56 | throw Error(`Unauthorized format specified in \`configFormat\`. afterConvert: ${convertArray[1]}`) 57 | 58 | extension = convertArray[1] 59 | } 60 | } 61 | 62 | const outputDir = `/${ 63 | config.imageDir ? config.imageDir.replace(/^\//, '').replace(/\/$/, '') : '_next/static/chunks/images' 64 | }` 65 | 66 | const extensions = [...new Set([...(config.generateFormats ?? ['webp']), extension])] 67 | return extensions.map((extension, index) => { 68 | if (extensions.length !== index + 1 && !formatValidate(extension)) 69 | throw Error(`Unauthorized extension specified in \`generateFormats\`: ${extension}`) 70 | 71 | const filename = 72 | config.filenameGenerator !== undefined 73 | ? config.filenameGenerator({ path: pathWithoutName, name, width, extension }) 74 | : `${pathWithoutName}/${name}_${width}.${extension}` 75 | const output = `${outputDir}/${filename.replace(/^\//, '')}` 76 | 77 | return { output, src, extension, originalExtension } 78 | }) 79 | } 80 | 81 | export default buildOutputInfo 82 | -------------------------------------------------------------------------------- /src/utils/formatValidate.ts: -------------------------------------------------------------------------------- 1 | const formats = ['jpeg', 'jpg', 'png', 'webp', 'avif'] as const 2 | export type AllowedFormat = (typeof formats)[number] 3 | 4 | const formatValidate = (format?: string): format is AllowedFormat => 5 | formats.some((allowedFormat) => allowedFormat === format) 6 | 7 | export default formatValidate 8 | -------------------------------------------------------------------------------- /src/utils/getConfig.ts: -------------------------------------------------------------------------------- 1 | import type { AvifOptions, JpegOptions, PngOptions, WebpOptions } from 'sharp' 2 | import type { AllowedFormat } from './formatValidate' 3 | 4 | type ParsedImageInfo = { 5 | pathWithoutName: string 6 | name: string 7 | extension: string 8 | } 9 | export type DefaultImageParser = (src: string) => ParsedImageInfo 10 | type SourceImageParser = (determinerProps: { src: string; defaultParser: DefaultImageParser }) => ParsedImageInfo 11 | 12 | export type Config = { 13 | /** 14 | * Specify if you are customizing the default output directory, such as next export -o outDir. 15 | * 16 | * @type {string} 17 | */ 18 | outDir?: string 19 | /** 20 | * You can customize the directory to output optimized images. 21 | * The default is '_next/static/chunks/images'. 22 | * 23 | * @type {string} 24 | */ 25 | imageDir?: string 26 | /** 27 | * You can customize the directory to cache images. 28 | * The default is 'node_modules/.cache'. 29 | * 30 | * @type {string} 31 | */ 32 | cacheDir?: string 33 | /** 34 | * Images in the public directory are automatically optimized, but if there are any images you want to ignore the optimization for, please specify the path. 35 | * Please specify a relative path from the public directory. 36 | */ 37 | ignorePaths?: string[] 38 | /** 39 | * Required if you have set basePath in next.config.js. 40 | * Please set the same value. 41 | * 42 | * @type {string} 43 | */ 44 | basePath?: string 45 | /** 46 | * You can customize the directory to output downloaded external images. 47 | * The default is '_next/static/media' 48 | * 49 | * @type {string} 50 | */ 51 | externalImageDir?: string 52 | /** 53 | * You can customize the quality of the optimized image. 54 | * The default is 75. 55 | */ 56 | quality?: number 57 | /** 58 | * You can customize the generation of file names. 59 | * 60 | * ❗️Attention 61 | * When making this setting, make sure that the file names (including the path part) of different images do not cover each other. 62 | * Specifically, include the name, width, and extension in the return value. If path is not included, all src's should be specified with import or require so that they can be distinguished by their hash value even if they have the same filename. 63 | * 64 | * @type {({ path: string, name: string, width: number, extension: string }) => string} 65 | */ 66 | filenameGenerator?: (generatorProps: { path: string; name: string; width: number; extension: string }) => string 67 | /** 68 | * You can set optimization options for each extension. 69 | * Please refer to the official sharp documentation for more information. 70 | * 71 | * @type {{ png?: PngOptions, jpg?: JpegOptions, webp?: WebpOptions, avif?: AvifOptions } }} 72 | */ 73 | sharpOptions?: { 74 | png?: PngOptions 75 | jpg?: JpegOptions 76 | webp?: WebpOptions 77 | avif?: AvifOptions 78 | } 79 | /** 80 | * It allows you to convert images from any extension to another extension. 81 | * 82 | * @type {[beforeConvert: AllowedFormat, afterConvert: AllowedFormat][]} 83 | */ 84 | convertFormat?: [beforeConvert: AllowedFormat, afterConvert: AllowedFormat][] 85 | 86 | /** 87 | * You can generate extra images in extensions specified. 88 | * The default is ['webp']. 89 | * This setting affects the extension displayed in the `Picture` component. 90 | * The order is also important. For example, if `webp` is first, then `webp` will be displayed first. 91 | * 92 | * @type {('webp' | 'avif')[]} 93 | */ 94 | generateFormats?: ('webp' | 'avif')[] 95 | 96 | /** 97 | * Allows you to optionally override the parsed image information before optimized images. 98 | * 99 | * @type {SourceImageParser} 100 | */ 101 | sourceImageParser?: SourceImageParser 102 | 103 | /** 104 | * You can directly specify the URL of an external image. 105 | * This is useful in cases where it is not known what images will be used for the build using variables, for example. 106 | * 107 | * @type {string[] | (() => string[] | Promise)} 108 | */ 109 | remoteImages?: string[] | (() => string[] | Promise) 110 | 111 | /** 112 | * In case you need to download a large amount of images from an external CDN with a rate limit, this will add delays between downloading images. 113 | */ 114 | remoteImagesDownloadsDelay?: number 115 | 116 | /** 117 | * You can specify the mode to use. The default is 'export'. 118 | * 'build' mode is for use with `next build` and `next start`. 119 | * 120 | * @type {('build' | 'export')} 121 | */ 122 | mode?: 'build' | 'export' 123 | } 124 | 125 | type ResolvedConfig = Config & { 126 | remoteImages?: string[] 127 | } 128 | 129 | const getConfig = (): ResolvedConfig => { 130 | // eslint-disable-next-line @typescript-eslint/no-var-requires 131 | const config = require('next-export-optimize-images/export-images.config.js') as Omit< 132 | ResolvedConfig, 133 | 'filenameGenerator' | 'sourceImageParser' 134 | > & { 135 | filenameGenerator?: string 136 | sourceImageParser?: string 137 | } 138 | 139 | return { 140 | ...config, 141 | filenameGenerator: config.filenameGenerator 142 | ? Function(`"use strict";return (${config.filenameGenerator})`)() 143 | : undefined, 144 | sourceImageParser: config.sourceImageParser 145 | ? Function(`"use strict";return (${config.sourceImageParser})`)() 146 | : undefined, 147 | } 148 | } 149 | 150 | export default getConfig 151 | -------------------------------------------------------------------------------- /src/utils/parseNdJSON.ts: -------------------------------------------------------------------------------- 1 | const parseNdJSON = (ndjson: string) => 2 | ndjson 3 | .trim() 4 | .split(/\n/g) 5 | .map((line) => JSON.parse(line)) 6 | 7 | export default parseNdJSON 8 | -------------------------------------------------------------------------------- /src/utils/processManifest.ts: -------------------------------------------------------------------------------- 1 | import type { Manifest } from '../cli' 2 | import parseNdJSON from './parseNdJSON' 3 | 4 | const processManifest = (manifestJson: string): Manifest => parseNdJSON(manifestJson) 5 | 6 | export default processManifest 7 | -------------------------------------------------------------------------------- /src/withExportImages.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import colors from 'ansi-colors' 3 | import appRootPath from 'app-root-path' 4 | import fs from 'fs-extra' 5 | import type { NextConfig } from 'next' 6 | import type { Config } from './utils/getConfig' 7 | 8 | // Track whether the configuration message has been logged 9 | let configMessageLogged = false 10 | 11 | const withExportImages = async ( 12 | nextConfig: NextConfig = {}, 13 | options: { __test?: boolean } = {} 14 | ): Promise => { 15 | if (nextConfig.images?.unoptimized) { 16 | throw Error( 17 | 'The `images.unoptimized` is not supported. If you use this option, consider not using `next-export-optimize-images`.' 18 | ) 19 | } 20 | 21 | const resolvedConfigPathOfDefault = path.join(process.cwd(), 'export-images.config.js') 22 | const resolvedConfigPathOfCjs = path.join(process.cwd(), 'export-images.config.cjs') 23 | const existConfigOfDefault = fs.existsSync(resolvedConfigPathOfDefault) 24 | const existConfigOfCjs = fs.existsSync(resolvedConfigPathOfCjs) 25 | const resolvedConfigPath = existConfigOfDefault 26 | ? resolvedConfigPathOfDefault 27 | : existConfigOfCjs 28 | ? resolvedConfigPathOfCjs 29 | : null 30 | const destConfigPath = appRootPath.resolve('node_modules/next-export-optimize-images/export-images.config.js') 31 | 32 | let config: Config = {} 33 | if (resolvedConfigPath !== null) { 34 | // eslint-disable-next-line @typescript-eslint/no-var-requires 35 | const configSrc = require(resolvedConfigPath) as Config 36 | config = configSrc 37 | if (configSrc.remoteImages) { 38 | if (typeof configSrc.remoteImages === 'function') { 39 | config.remoteImages = await configSrc.remoteImages() 40 | } 41 | } 42 | } 43 | 44 | fs.ensureFileSync(destConfigPath) 45 | fs.writeFileSync( 46 | destConfigPath, 47 | `module.exports = ${JSON.stringify( 48 | config, 49 | (k, v) => { 50 | if (k === 'filenameGenerator' || k === 'sourceImageParser') { 51 | return v.toString() 52 | } 53 | return v 54 | }, 55 | 2 56 | )}` 57 | ) 58 | 59 | if (!configMessageLogged) { 60 | // eslint-disable-next-line no-console 61 | console.log( 62 | colors.magenta( 63 | `info - [next-export-optimize-images]: ${ 64 | resolvedConfigPath !== null 65 | ? `Configuration loaded from \`${resolvedConfigPath}\`.` 66 | : 'Configuration was not loaded. (This is optional.)' 67 | }` 68 | ) 69 | ) 70 | configMessageLogged = true 71 | } 72 | 73 | const customConfig: NextConfig = { 74 | webpack(config, option) { 75 | const nextImageLoader = config.module.rules.find( 76 | ({ loader }: { loader?: string }) => loader === 'next-image-loader' 77 | ) 78 | 79 | config.module.rules = [ 80 | ...config.module.rules.filter(({ loader }: { loader?: string }) => loader !== 'next-image-loader'), 81 | { 82 | ...nextImageLoader, 83 | loader: undefined, 84 | options: undefined, 85 | use: [ 86 | { 87 | loader: 'next-export-optimize-images-loader', 88 | options: { 89 | dir: path.join(process.cwd(), options.__test ? '__tests__/e2e' : ''), 90 | isDev: option.dev, 91 | }, 92 | }, 93 | { loader: nextImageLoader.loader, options: nextImageLoader.options }, 94 | ], 95 | }, 96 | ] 97 | 98 | config.resolveLoader.alias['next-export-optimize-images-loader'] = options.__test 99 | ? path.join(__dirname, 'loader') 100 | : 'next-export-optimize-images/dist/loader' 101 | 102 | return nextConfig.webpack ? nextConfig.webpack(config, option) : config 103 | }, 104 | images: { 105 | ...nextConfig.images, 106 | loader: nextConfig.images?.loader ?? 'custom', 107 | }, 108 | } 109 | 110 | return Object.assign({}, nextConfig, customConfig) 111 | } 112 | 113 | export default withExportImages 114 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/strictest/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES2020", 5 | "moduleResolution": "node", 6 | "allowJs": true, 7 | "noEmit": true, 8 | "jsx": "react", 9 | "baseUrl": ".", 10 | "noPropertyAccessFromIndexSignature": false 11 | }, 12 | "include": ["src/**/*", "__tests__/**/*", "types/**/*"], 13 | "exclude": ["node_modules", "dist", "__tests__/e2e/**/*", "__tests__/e2e-build/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | const cfg = { 4 | splitting: false, 5 | sourcemap: true, 6 | clean: true, 7 | treeshake: false, 8 | dts: true, 9 | format: ['cjs'], 10 | } 11 | 12 | export default defineConfig([ 13 | // Main 14 | { 15 | ...cfg, 16 | entry: { 17 | index: 'src/index.ts', 18 | cli: 'src/cli/index.ts', 19 | loader: 'src/loader/index.ts', 20 | }, 21 | external: ['next-export-optimize-images'], 22 | outDir: 'dist', 23 | }, 24 | 25 | // Server Components 26 | { 27 | ...cfg, 28 | entry: { 29 | 'remote-image': 'src/components/server/remote-image.tsx', 30 | 'remote-picture': 'src/components/server/remote-picture.tsx', 31 | }, 32 | external: ['next-export-optimize-images', '../client/image', '../client/picture'], 33 | outDir: 'dist/components/server', 34 | }, 35 | 36 | // Client Components 37 | { 38 | ...cfg, 39 | entry: { 40 | image: 'src/components/client/image.tsx', 41 | 'legacy/image': 'src/components/client/legacy/image.tsx', 42 | picture: 'src/components/client/picture.tsx', 43 | }, 44 | external: ['next-export-optimize-images'], 45 | outDir: 'dist/components/client', 46 | esbuildOptions: (options) => { 47 | // Append "use client" to the top of the react entry point 48 | options.banner = { 49 | js: '"use client";', 50 | } 51 | }, 52 | }, 53 | ]) 54 | --------------------------------------------------------------------------------