├── .nvmrc ├── .gitattributes ├── .husky ├── pre-commit └── commit-msg ├── .npmrc ├── .vscode └── settings.json ├── .github ├── ISSUE_TEMPLATE │ ├── config.yaml │ ├── feature_request.yaml │ └── bug_report.yaml ├── workflows │ ├── clear_gh_caches.yaml │ └── validate_build.yaml ├── pull_request_template.md └── renovate.json5 ├── .prettierignore ├── .prettierrc.js ├── .editorconfig ├── lint-staged.config.js ├── .gitignore ├── tsconfig.json ├── LICENSE ├── .commitlintrc.js ├── .release-it.js ├── vite.config.ts ├── eslint.config.js ├── src ├── constants.ts ├── utils.ts └── index.ts ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.16.0 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit "${1}" 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shell-emulator=true 2 | publish-branch=main 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .idea 4 | .vscode 5 | reports 6 | tar 7 | pnpm-lock.yaml 8 | renovate.json5 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://prettier.io/docs/configuration 3 | * @type {import('prettier').Config} 4 | */ 5 | export default { 6 | trailingComma: 'es5', 7 | semi: true, 8 | singleQuote: true, 9 | arrowParens: 'avoid', 10 | }; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 140 11 | 12 | [*.{markdown,md}] 13 | trim_trailing_whitespace = false 14 | max_line_length = 200 15 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | const prettierCmd = `prettier --log-level warn --cache --cache-strategy content --cache-location ./node_modules/.cache/.prettiercache --write`; 2 | const eslintCmd = `eslint --max-warnings=0 --format=pretty --cache --cache-strategy content --cache-location ./node_modules/.cache/.eslintcache --fix`; 3 | 4 | /** @type {import('lint-staged').Configuration} */ 5 | export default { 6 | '**/*.{js,ts}': [eslintCmd, prettierCmd], 7 | '**/*.{md,json,yaml,yml}': [prettierCmd], 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # ide 3 | .idea 4 | .vscode/* 5 | !.vscode/settings.json 6 | !.vscode/tasks.json 7 | !.vscode/launch.json 8 | !.vscode/extensions.json 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # env files 17 | .env.development.local 18 | .env.production.local 19 | 20 | # Dependency directories 21 | node_modules 22 | .pnp.* 23 | .pnpm-store/ 24 | 25 | # compiled output 26 | /dist 27 | /build 28 | /tar 29 | *.tgz 30 | 31 | # Logs 32 | logs 33 | *.log 34 | .pnpm-debug.log* 35 | 36 | # Reports 37 | reports 38 | 39 | # System Files 40 | .DS_Store 41 | Thumbs.db 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "lib": ["ESNext"], 8 | "types": ["node"], 9 | 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "resolveJsonModule": true, 13 | "sourceMap": false, 14 | "declaration": true, 15 | "emitDeclarationOnly": true, 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": true, 18 | "experimentalDecorators": true, 19 | 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "erasableSyntaxOnly": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noUncheckedSideEffectImports": true 26 | }, 27 | "include": ["src/index.ts"] 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/clear_gh_caches.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | schedule: 4 | # At 00:00 on day-of-month 1 in every 2nd month. (i.e every 2 months) 5 | - cron: '0 0 1 */2 *' 6 | 7 | name: clear_gh_caches 8 | 9 | jobs: 10 | cleanup: 11 | env: 12 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Cleanup 18 | run: | 19 | gh extension install actions/gh-actions-cache 20 | 21 | REPO=${{ github.repository }} 22 | 23 | echo "Fetching list of cache keys" 24 | cacheKeys=$(gh actions-cache list -R $REPO | cut -f 1 ) 25 | 26 | ## Do not fail the workflow while deleting cache keys. 27 | set +e 28 | echo "Deleting caches..." 29 | for cacheKey in $cacheKeys 30 | do 31 | gh actions-cache delete $cacheKey -R $REPO --confirm 32 | done 33 | echo "Done" 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 FatehAK 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 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ### What is the purpose of this pull request? 8 | 9 | - [ ] Bug fix 10 | - [ ] New Feature 11 | - [ ] Documentation 12 | - [ ] Other 13 | 14 | ### Description 15 | 16 | 17 | 18 | ### Checks 19 | 20 | - [ ] PR adheres to the code style of this project 21 | - [ ] Related issues linked using `fixes #number` 22 | - [ ] Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate. 23 | - [ ] Lint and build have passed locally by running `pnpm lint && pnpm build` 24 | - [ ] Code changes have been verified in local 25 | - [ ] Documentation added/updated if necessary 26 | 27 | ### Additional Context 28 | 29 | 30 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('cz-git').UserConfig} */ 2 | export default { 3 | rules: { 4 | 'type-enum': [2, 'always', ['feat', 'fix', 'build', 'chore', 'ci', 'docs', 'revert']], 5 | 'type-case': [2, 'always', 'lower-case'], 6 | 'type-empty': [2, 'never'], 7 | 'scope-case': [2, 'always', 'lower-case'], 8 | 'subject-empty': [2, 'never'], 9 | 'subject-full-stop': [2, 'never', '.'], 10 | 'subject-max-length': [2, 'always', 100], 11 | }, 12 | prompt: { 13 | messages: { 14 | type: 'Type of change:', 15 | subject: 'Commit Message:', 16 | footer: 'Any ISSUES related to this change:', 17 | }, 18 | types: [ 19 | { value: 'feat', name: '✨ feat' }, 20 | { value: 'fix', name: '🐛 fix' }, 21 | { value: 'build', name: '📦️ build' }, 22 | { value: 'chore', name: '🔨 chore' }, 23 | { value: 'ci', name: '🎡 ci' }, 24 | { value: 'docs', name: '📝 docs' }, 25 | { value: 'revert', name: '⏪️ revert' }, 26 | ], 27 | useEmoji: false, 28 | upperCaseSubject: true, 29 | skipQuestions: ['body', 'scope', 'breaking', 'footerPrefix', 'confirmCommit'], 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /.release-it.js: -------------------------------------------------------------------------------- 1 | /** @type {import('release-it').Config} */ 2 | export default { 3 | git: { 4 | requireBranch: 'main', 5 | commitMessage: 'chore: Release v${version}', 6 | tagName: 'v${version}', 7 | requireCommits: true, // require commits since last tag 8 | requireCleanWorkingDir: true, // exits if local not upto date with remote or if workdir is unclean 9 | }, 10 | github: { 11 | release: true, // creates a github release 12 | draft: true, // github releases are only drafted, confirm the draft in github releases page to publish it 13 | commitArgs: ['-S'], // creates gpg signed commits 14 | tagArgs: ['-s'], // creates gpg signed tags 15 | releaseName: '✨ v${version}', 16 | assets: ['tar/*.tgz'], 17 | }, 18 | npm: { 19 | publish: true, 20 | }, 21 | hooks: { 22 | // runs lint before releasing 23 | 'before:init': ['pnpm lint'], 24 | // build the package and generate a tarball for use in github releases 25 | 'after:bump': 'pnpm build && pnpm tarball', 26 | 'after:release': 'echo Successfully created a release v${version} for ${repo.repository}. Please add release notes and publish it!', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig, type LibraryFormats } from 'vite'; 3 | import { visualizer } from 'rollup-plugin-visualizer'; 4 | import dts from 'vite-plugin-dts'; 5 | import pkg from './package.json'; 6 | 7 | /** @type {import('vite').UserConfig} */ 8 | export default defineConfig(() => { 9 | return { 10 | plugins: [ 11 | dts({ 12 | insertTypesEntry: true, 13 | copyDtsFiles: true, 14 | }), 15 | ], 16 | build: { 17 | minify: false, 18 | lib: { 19 | entry: resolve(__dirname, 'src/index.ts'), 20 | name: 'ViteImageOptimizer', 21 | formats: ['es', 'cjs'] as LibraryFormats[], 22 | fileName: 'index', 23 | }, 24 | rollupOptions: { 25 | external: ['fs', 'fs/promises', 'svgo', 'sharp', ...Object.keys(pkg.dependencies)], 26 | plugins: [ 27 | visualizer({ 28 | filename: 'reports/build-stats.html', 29 | gzipSize: true, 30 | brotliSize: true, 31 | }), 32 | ], 33 | output: { 34 | globals: { 35 | fs: 'fs', 36 | 'fs/promises': 'fsp', 37 | svgo: 'svgo', 38 | sharp: 'sharp', 39 | }, 40 | }, 41 | }, 42 | }, 43 | }; 44 | }); 45 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: ["config:base"], 4 | labels: ["dependencies"], 5 | timezone: "Asia/Kolkata", 6 | schedule: ["before 11am on saturday"], 7 | enabledManagers: ["npm"], 8 | rangeStrategy: "bump", 9 | commitMessagePrefix: "chore(deps): ", 10 | commitBodyTable: true, 11 | dependencyDashboard: true, 12 | dependencyDashboardAutoclose: true, 13 | configMigration: true, 14 | automerge: true, 15 | automergeType: "branch", 16 | automergeStrategy: "rebase", 17 | platformCommit: true, 18 | lockFileMaintenance: { 19 | enabled: true, 20 | }, 21 | rebaseWhen: "behind-base-branch", 22 | patch: { 23 | groupName: "non-major dependencies", 24 | groupSlug: "minor-patch", 25 | }, 26 | minor: { 27 | groupName: "non-major dependencies", 28 | groupSlug: "minor-patch", 29 | }, 30 | major: { 31 | automerge: false, 32 | dependencyDashboardApproval: true, 33 | commitMessagePrefix: "chore(deps-major): ", 34 | labels: ["dependencies", "breaking"], 35 | }, 36 | vulnerabilityAlerts: { 37 | labels: ["security"], 38 | }, 39 | packageRules: [ 40 | { 41 | matchPackageNames: ["node"], 42 | enabled: false, 43 | }, 44 | { 45 | matchDepTypes: ["peerDependencies"], 46 | enabled: false, 47 | }, 48 | ], 49 | } 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 New feature proposal" 2 | description: Propose a new feature 3 | labels: ['enhancement'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to file a feature request! Please fill out this form as completely as possible. 9 | - type: textarea 10 | id: feature-description 11 | attributes: 12 | label: Describe the feature you'd like to request 13 | description: A clear and concise description of what you want and what your use case is. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: suggested-solution 18 | attributes: 19 | label: Describe the solution you'd like 20 | description: A clear and concise description of what you want to happen. 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: additional-context 25 | attributes: 26 | label: Additional context 27 | description: Any other context or screenshots about the feature request here. 28 | - type: checkboxes 29 | id: checkboxes 30 | attributes: 31 | label: Validations 32 | description: Before submitting the issue, please make sure you do the following 33 | options: 34 | - label: Read the [docs](https://github.com/FatehAK/vite-plugin-image-optimizer#readme). 35 | required: true 36 | - label: Check that there isn't [already an issue](https://github.com/FatehAK/vite-plugin-image-optimizer/issues) that requests the same feature to avoid creating a duplicate. 37 | required: true 38 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, globalIgnores } from 'eslint/config'; 2 | import { FlatCompat } from '@eslint/eslintrc'; 3 | import globals from 'globals'; 4 | import onlyWarn from 'eslint-plugin-only-warn'; 5 | import tsParser from '@typescript-eslint/parser'; 6 | import js from '@eslint/js'; 7 | 8 | const __dirname = import.meta.dirname; 9 | 10 | const compat = new FlatCompat({ 11 | baseDirectory: __dirname, 12 | recommendedConfig: js.configs.recommended, 13 | allConfig: js.configs.all, 14 | }); 15 | 16 | /** @type {import('eslint').Linter.Config[]} */ 17 | export default defineConfig([ 18 | { 19 | languageOptions: { 20 | globals: { 21 | ...globals.node, 22 | }, 23 | ecmaVersion: 'latest', 24 | sourceType: 'module', 25 | parserOptions: {}, 26 | }, 27 | extends: compat.extends('plugin:sonarjs/recommended-legacy', 'plugin:promise/recommended', 'prettier'), 28 | rules: { 29 | 'sonarjs/no-duplicate-string': 'off', 30 | 'sonarjs/no-nested-template-literals': 'off', 31 | 'sonarjs/cognitive-complexity': 'off', 32 | 'global-require': 'off', 33 | 'no-restricted-exports': 'off', 34 | 'no-console': 'off', 35 | 'func-names': 'off', 36 | 'no-template-curly-in-string': 'off', 37 | 'no-unused-vars': [ 38 | 'error', 39 | { 40 | argsIgnorePattern: '^_', 41 | }, 42 | ], 43 | }, 44 | plugins: { 45 | 'only-warn': onlyWarn, 46 | }, 47 | }, 48 | { 49 | files: ['**/*.ts'], 50 | languageOptions: { 51 | parser: tsParser, 52 | }, 53 | }, 54 | globalIgnores(['**/node_modules', '**/dist', '**/.idea', '**/.vscode', '**/reports', '**/tar', '!**/*.js', '!**/*.ts']), 55 | ]); 56 | -------------------------------------------------------------------------------- /.github/workflows/validate_build.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - 'main' 5 | - 'renovate/**' 6 | paths-ignore: 7 | - '**.md' 8 | pull_request: 9 | branches: 10 | - 'main' 11 | paths-ignore: 12 | - '**.md' 13 | 14 | name: validate_build 15 | 16 | jobs: 17 | lint: 18 | name: Lint 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: pnpm/action-setup@v4 23 | 24 | - name: Setup Node and pnpm-store cache 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version-file: '.nvmrc' 28 | cache: 'pnpm' 29 | 30 | - name: Dump versions 31 | run: | 32 | echo "node: $(node -v)" 33 | echo "npm: $(npm -v)" 34 | echo "pnpm: $(pnpm -v)" 35 | 36 | - name: Install dependencies 37 | run: pnpm install --frozen-lockfile 38 | 39 | - name: Setup script cache 40 | uses: actions/cache@v4 41 | with: 42 | path: node_modules/.cache 43 | key: script-cache-${{ hashFiles('**/pnpm-lock.yaml') }}-run-id-${{ github.run_id }} 44 | restore-keys: script-cache-${{ hashFiles('**/pnpm-lock.yaml') }}-run-id- 45 | 46 | - name: Run lint 47 | run: pnpm lint 48 | 49 | build: 50 | name: Build 51 | needs: lint 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v4 55 | - uses: pnpm/action-setup@v4 56 | 57 | - name: Setup Node and pnpm-store cache 58 | uses: actions/setup-node@v4 59 | with: 60 | node-version-file: '.nvmrc' 61 | cache: 'pnpm' 62 | 63 | - name: Dump versions 64 | run: | 65 | echo "node: $(node -v)" 66 | echo "npm: $(npm -v)" 67 | echo "pnpm: $(pnpm -v)" 68 | 69 | - name: Install dependencies 70 | run: pnpm install --frozen-lockfile 71 | 72 | - name: Build 73 | run: pnpm build 74 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import type { Config as SVGOConfig } from 'svgo'; 2 | import type { PngOptions, JpegOptions, TiffOptions, GifOptions, WebpOptions, AvifOptions } from 'sharp'; 3 | 4 | export const VITE_PLUGIN_NAME = 'vite-plugin-image-optimizer'; 5 | 6 | const SVGO_CONFIG: SVGOConfig = { 7 | multipass: true, 8 | plugins: [ 9 | { 10 | name: 'preset-default', 11 | params: { 12 | overrides: { 13 | cleanupNumericValues: false, 14 | cleanupIds: { 15 | minify: false, 16 | remove: false, 17 | }, 18 | convertPathData: false, 19 | }, 20 | }, 21 | }, 22 | 'sortAttrs', 23 | { 24 | name: 'addAttributesToSVGElement', 25 | params: { 26 | attributes: [{ xmlns: 'http://www.w3.org/2000/svg' }], 27 | }, 28 | }, 29 | ], 30 | }; 31 | 32 | export const DEFAULT_OPTIONS = { 33 | logStats: true, 34 | ansiColors: true, 35 | includePublic: true, 36 | exclude: undefined, 37 | include: undefined, 38 | test: /\.(jpe?g|png|gif|tiff|webp|svg|avif)$/i, 39 | svg: SVGO_CONFIG, 40 | png: { 41 | // https://sharp.pixelplumbing.com/api-output#png 42 | quality: 100, 43 | } as PngOptions, 44 | jpeg: { 45 | // https://sharp.pixelplumbing.com/api-output#jpeg 46 | quality: 100, 47 | } as JpegOptions, 48 | jpg: { 49 | // https://sharp.pixelplumbing.com/api-output#jpeg 50 | quality: 100, 51 | } as JpegOptions, 52 | tiff: { 53 | // https://sharp.pixelplumbing.com/api-output#tiff 54 | quality: 100, 55 | } as TiffOptions, 56 | // gif does not support lossless compression 57 | // https://sharp.pixelplumbing.com/api-output#gif 58 | gif: {} as GifOptions, 59 | webp: { 60 | // https://sharp.pixelplumbing.com/api-output#webp 61 | lossless: true, 62 | } as WebpOptions, 63 | avif: { 64 | // https://sharp.pixelplumbing.com/api-output#avif 65 | lossless: true, 66 | } as AvifOptions, 67 | cache: false, 68 | cacheLocation: undefined, 69 | }; 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41E Bug report" 2 | description: Report an issue 3 | labels: [bug, need testing] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! 9 | - type: textarea 10 | id: bug-description 11 | attributes: 12 | label: Describe the Bug 13 | description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks! 14 | placeholder: | 15 | When I do , happens and I see the error message attached below: 16 | ```...``` 17 | What I expect is 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: reproduction-steps 22 | attributes: 23 | label: Steps to reproduce 24 | description: Please provide a clear description of how to reproduce the issue. Screenshots can be provided in the issue body below. 25 | If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken. 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: system-info 30 | attributes: 31 | label: System Info 32 | description: Output of `npx envinfo --system --npmPackages '{vite,@vitejs/}*' --binaries --browsers` 33 | render: shell 34 | placeholder: System, Binaries, Browsers 35 | validations: 36 | required: true 37 | - type: dropdown 38 | id: package-manager 39 | attributes: 40 | label: Used Package Manager 41 | description: Select the used package manager 42 | options: 43 | - npm 44 | - yarn 45 | - pnpm 46 | validations: 47 | required: true 48 | - type: checkboxes 49 | id: checkboxes 50 | attributes: 51 | label: Validations 52 | description: Before submitting the issue, please make sure you do the following 53 | options: 54 | - label: Read the [docs](https://github.com/FatehAK/vite-plugin-image-optimizer#readme). 55 | required: true 56 | - label: Check that there isn't [already an issue](https://github.com/FatehAK/vite-plugin-image-optimizer/issues) that reports the same bug to avoid creating a duplicate. 57 | required: true 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-image-optimizer", 3 | "version": "2.0.3", 4 | "author": "fatehak", 5 | "type": "module", 6 | "files": [ 7 | "dist/" 8 | ], 9 | "types": "./dist/index.d.ts", 10 | "main": "./dist/index.js", 11 | "license": "MIT", 12 | "exports": { 13 | ".": { 14 | "types": "./dist/index.d.ts", 15 | "import": "./dist/index.js", 16 | "require": "./dist/index.cjs" 17 | }, 18 | "./package.json": "./package.json" 19 | }, 20 | "description": "A Vite plugin to optimize your image assets using Sharp.js and SVGO", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/FatehAK/vite-plugin-image-optimizer" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/FatehAK/vite-plugin-image-optimizer/issues" 27 | }, 28 | "homepage": "https://github.com/FatehAK/vite-plugin-image-optimizer#readme", 29 | "keywords": [ 30 | "vite", 31 | "vite-plugin", 32 | "image compression", 33 | "image optimization", 34 | "svgo", 35 | "sharp.js" 36 | ], 37 | "scripts": { 38 | "start": "vite", 39 | "build": "vite build", 40 | "build:watch": "vite build --watch", 41 | "release": "release-it", 42 | "tarball": "rimraf tar && pnpm pack --pack-destination ./tar", 43 | "analyze": "open ./reports/build-stats.html", 44 | "clean": "pnpm rimraf reports dist node_modules/.vite node_modules/.cache", 45 | "lint": "concurrently -g -n \"prettier,eslint\" -c \"bgGreen.bold,bgBlue.bold\" \"pnpm prettier-check\" \"pnpm eslint-check\"", 46 | "lint:fix": "pnpm eslint-fix && pnpm prettier-fix", 47 | "prettier-check": "prettier --log-level warn --cache --cache-strategy content --cache-location ./node_modules/.cache/.prettiercache --check .", 48 | "prettier-fix": "prettier --log-level warn --cache --cache-strategy content --cache-location ./node_modules/.cache/.prettiercache --write .", 49 | "eslint-check": "eslint --max-warnings=25 --format=pretty --cache --cache-strategy content --cache-location ./node_modules/.cache/.eslintcache '{**/*,*}.{js,ts}'", 50 | "eslint-fix": "pnpm eslint-check --fix", 51 | "lint-staged": "lint-staged", 52 | "commit": "git-cz", 53 | "prepare": "husky" 54 | }, 55 | "config": { 56 | "commitizen": { 57 | "path": "node_modules/cz-git" 58 | } 59 | }, 60 | "dependencies": { 61 | "ansi-colors": "^4.1.3", 62 | "pathe": "^2.0.3" 63 | }, 64 | "devDependencies": { 65 | "@commitlint/cli": "^19.8.1", 66 | "@commitlint/types": "^19.8.1", 67 | "@eslint/eslintrc": "^3.3.3", 68 | "@eslint/js": "^9.39.2", 69 | "@types/node": "^24.10.4", 70 | "@typescript-eslint/parser": "^8.50.0", 71 | "concurrently": "^9.2.1", 72 | "cz-git": "^1.12.0", 73 | "eslint": "^9.39.2", 74 | "eslint-config-prettier": "^10.1.8", 75 | "eslint-formatter-pretty": "^6.0.1", 76 | "eslint-plugin-only-warn": "^1.1.0", 77 | "eslint-plugin-promise": "^7.2.1", 78 | "eslint-plugin-sonarjs": "^3.0.5", 79 | "globals": "^16.5.0", 80 | "husky": "^9.1.7", 81 | "lint-staged": "^16.2.7", 82 | "prettier": "^3.7.4", 83 | "release-it": "^19.1.0", 84 | "rimraf": "^6.1.2", 85 | "rollup-plugin-visualizer": "^6.0.5", 86 | "sharp": "^0.34.5", 87 | "svgo": "^4.0.0", 88 | "typescript": "^5.9.3", 89 | "vite": "^6.4.1", 90 | "vite-plugin-dts": "^4.5.4" 91 | }, 92 | "peerDependencies": { 93 | "vite": ">=5", 94 | "svgo": ">=4", 95 | "sharp": ">=0.34.0" 96 | }, 97 | "peerDependenciesMeta": { 98 | "svgo": { 99 | "optional": true 100 | }, 101 | "sharp": { 102 | "optional": true 103 | } 104 | }, 105 | "engines": { 106 | "node": ">=18.17.0" 107 | }, 108 | "packageManager": "pnpm@10.26.1", 109 | "publishConfig": { 110 | "registry": "https://registry.npmjs.org" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ResolvedConfig } from 'vite'; 2 | import fs from 'fs'; 3 | import { join, basename } from 'pathe'; 4 | import ansi from 'ansi-colors'; 5 | 6 | interface Sizes { 7 | size: number; 8 | oldSize: number; 9 | ratio: number; 10 | skipWrite: boolean; 11 | isCached: boolean; 12 | } 13 | 14 | type Match = string | RegExp | string[]; 15 | 16 | /* type utils */ 17 | function isRegex(src: Match) { 18 | return Object.prototype.toString.call(src) === '[object RegExp]'; 19 | } 20 | 21 | function isString(src: Match) { 22 | return Object.prototype.toString.call(src) === '[object String]'; 23 | } 24 | 25 | function isArray(src: Match) { 26 | return Array.isArray(src); 27 | } 28 | 29 | export function merge(src: any, target: any) { 30 | const deepClone = (src: any) => { 31 | if (typeof src !== 'object' || isRegex(src) || src === null) return src; 32 | const target: any = Array.isArray(src) ? [] : {}; 33 | for (const key in src) { 34 | const value = src[key]; 35 | target[key] = deepClone(value); 36 | } 37 | return target; 38 | }; 39 | 40 | const clone = deepClone(src); 41 | for (const key in target) { 42 | if (clone[key] === undefined) { 43 | clone[key] = target[key]; 44 | } 45 | } 46 | return clone; 47 | } 48 | 49 | export function readAllFiles(root: string) { 50 | let resultArr: string[] = []; 51 | try { 52 | if (fs.existsSync(root)) { 53 | const stat = fs.lstatSync(root); 54 | if (stat.isDirectory()) { 55 | const files = fs.readdirSync(root); 56 | files.forEach(function (file) { 57 | const t = readAllFiles(join(root, '/', file)); 58 | resultArr = resultArr.concat(t); 59 | }); 60 | } else { 61 | resultArr.push(root); 62 | } 63 | } 64 | } catch (error) { 65 | console.log(error); 66 | } 67 | 68 | return resultArr; 69 | } 70 | 71 | export function areFilesMatching(fileName: string, filePath: string, matcher: any): boolean { 72 | if (isString(matcher)) return fileName === matcher; 73 | if (isRegex(matcher)) return matcher.test(filePath); 74 | if (isArray(matcher)) return matcher.includes(fileName); 75 | return false; 76 | } 77 | 78 | /* loggers */ 79 | function decideStyle(text: string, enableColors: boolean) { 80 | return enableColors ? text : ansi.unstyle(text); 81 | } 82 | 83 | function getLogger(rootConfig: ResolvedConfig) { 84 | return rootConfig?.logger ?? console; 85 | } 86 | 87 | function getOutDir(rootConfig: ResolvedConfig) { 88 | return rootConfig?.build?.outDir ?? 'dist'; 89 | } 90 | 91 | export function logErrors(rootConfig: ResolvedConfig, errorsMap: Map, ansiColors: boolean) { 92 | const logger = getLogger(rootConfig); 93 | const outDir = getOutDir(rootConfig); 94 | 95 | logger.info(decideStyle(`\n🚨 ${ansi.red('[vite-plugin-image-optimizer]')} - errors during optimization: `, ansiColors)); 96 | 97 | const keyLengths: number[] = Array.from(errorsMap.keys(), (name: string) => name.length); 98 | const maxKeyLength: number = Math.max(...keyLengths); 99 | 100 | errorsMap.forEach((message: string, name: string) => { 101 | logger.error( 102 | decideStyle( 103 | `${ansi.dim(basename(outDir))}/${ansi.blueBright(name)}${' '.repeat(2 + maxKeyLength - name.length)} ${ansi.red(message)}`, 104 | ansiColors 105 | ) 106 | ); 107 | }); 108 | logger.info('\n'); 109 | } 110 | 111 | export function logOptimizationStats(rootConfig: ResolvedConfig, sizesMap: Map, ansiColors: boolean) { 112 | const logger = getLogger(rootConfig); 113 | const outDir = getOutDir(rootConfig); 114 | 115 | logger.info(decideStyle(`\n✨ ${ansi.cyan('[vite-plugin-image-optimizer]')} - optimized images successfully: `, ansiColors)); 116 | 117 | const keyLengths: number[] = Array.from(sizesMap.keys(), (name: string) => name.length); 118 | const valueLengths: number[] = Array.from(sizesMap.values(), (value: any) => `${Math.floor(100 * value.ratio)}`.length); 119 | 120 | const maxKeyLength: number = Math.max(...keyLengths); 121 | const valueKeyLength: number = Math.max(...valueLengths); 122 | 123 | let totalOriginalSize: number = 0; 124 | let totalSavedSize: number = 0; 125 | sizesMap.forEach((value, name) => { 126 | const { size, oldSize, ratio, skipWrite, isCached } = value; 127 | 128 | let percentChange: string; 129 | if (ratio > 0) { 130 | percentChange = ansi.red(`+${ratio}%`); 131 | } else if (ratio <= 0) { 132 | percentChange = ansi.green(`${ratio}%`); 133 | } else { 134 | percentChange = ''; 135 | } 136 | 137 | let sizeText: string; 138 | if (skipWrite) { 139 | sizeText = `${ansi.yellow.bold('skipped')} ${ansi.dim(`original: ${oldSize.toFixed(2)} kB <= optimized: ${size.toFixed(2)} kB`)}`; 140 | } else if (isCached) { 141 | sizeText = `${ansi.yellow.bold('cached')} ${ansi.dim(`original: ${oldSize.toFixed(2)} kB; cached: ${size.toFixed(2)} kB`)}`; 142 | } else { 143 | sizeText = ansi.dim(`${oldSize.toFixed(2)} kB ⭢ ${size.toFixed(2)} kB`); 144 | } 145 | 146 | logger.info( 147 | decideStyle( 148 | ansi.dim(basename(outDir)) + 149 | '/' + 150 | ansi.blueBright(name) + 151 | ' '.repeat(2 + maxKeyLength - name.length) + 152 | ansi.gray(`${percentChange} ${' '.repeat(valueKeyLength - `${ratio}`.length)}`) + 153 | ' ' + 154 | sizeText, 155 | ansiColors 156 | ) 157 | ); 158 | 159 | if (!skipWrite) { 160 | totalOriginalSize += oldSize; 161 | totalSavedSize += oldSize - size; 162 | } 163 | }); 164 | 165 | if (totalSavedSize > 0) { 166 | const savedText = `${totalSavedSize.toFixed(2)}kB`; 167 | const originalText = `${totalOriginalSize.toFixed(2)}kB`; 168 | const savingsPercent = `${Math.round((totalSavedSize / totalOriginalSize) * 100)}%`; 169 | logger.info( 170 | decideStyle(`\n💰 total savings = ${ansi.green(savedText)}/${ansi.green(originalText)} ≈ ${ansi.green(savingsPercent)}`, ansiColors) 171 | ); 172 | } 173 | 174 | logger.info('\n'); 175 | } 176 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, ResolvedConfig } from 'vite'; 2 | import type { PngOptions, JpegOptions, TiffOptions, GifOptions, WebpOptions, AvifOptions, FormatEnum } from 'sharp'; 3 | import type { Config as SVGOConfig } from 'svgo'; 4 | import fs from 'fs'; 5 | import fsp from 'fs/promises'; 6 | import { dirname, extname, join, resolve, sep } from 'pathe'; 7 | import { filename } from 'pathe/utils'; 8 | import { merge, readAllFiles, areFilesMatching, logErrors, logOptimizationStats } from './utils'; 9 | import { VITE_PLUGIN_NAME, DEFAULT_OPTIONS } from './constants'; 10 | 11 | interface Options { 12 | /** 13 | * test to match files against 14 | */ 15 | test?: RegExp; 16 | /** 17 | * files to include 18 | */ 19 | include?: RegExp | string | string[]; 20 | /** 21 | * files to exclude 22 | */ 23 | exclude?: RegExp | string | string[]; 24 | /** 25 | * include assets in public dir or not 26 | */ 27 | includePublic?: boolean; 28 | /** 29 | * display logs using colors or not 30 | */ 31 | ansiColors?: boolean; 32 | /** 33 | * log stats to the terminal or not 34 | */ 35 | logStats?: boolean; 36 | /** 37 | * svgo opts 38 | */ 39 | svg?: SVGOConfig; 40 | /** 41 | * sharp opts for png 42 | */ 43 | png?: PngOptions; 44 | /** 45 | * sharp opts for jpeg 46 | */ 47 | jpeg?: JpegOptions; 48 | /** 49 | * sharp opts for jpg 50 | */ 51 | jpg?: JpegOptions; 52 | /** 53 | * sharp opts for tiff 54 | */ 55 | tiff?: TiffOptions; 56 | /** 57 | * sharp opts for gif 58 | */ 59 | gif?: GifOptions; 60 | /** 61 | * sharp opts for webp 62 | */ 63 | webp?: WebpOptions; 64 | /** 65 | * sharp opts for avif 66 | */ 67 | avif?: AvifOptions; 68 | /** 69 | * cache optimized images or not 70 | */ 71 | cache?: boolean; 72 | /** 73 | * path to the cache directory 74 | */ 75 | cacheLocation?: string; 76 | } 77 | 78 | function ViteImageOptimizer(optionsParam: Options = {}): Plugin { 79 | const options: Options = merge(optionsParam, DEFAULT_OPTIONS); 80 | 81 | let outputPath: string; 82 | let publicDir: string; 83 | let rootConfig: ResolvedConfig; 84 | 85 | const sizesMap = new Map(); 86 | const mtimeCache = new Map(); 87 | const errorsMap = new Map(); 88 | 89 | /* SVGO transformation */ 90 | const applySVGO = async (filePath: string, buffer: Buffer): Promise => { 91 | const optimize = (await import('svgo')).optimize; 92 | return Buffer.from( 93 | optimize(buffer.toString(), { 94 | path: filePath, 95 | ...options.svg, 96 | }).data 97 | ); 98 | }; 99 | 100 | /* Sharp transformation */ 101 | const applySharp = async (filePath: string, buffer: Buffer): Promise => { 102 | const sharp = (await import('sharp')).default; 103 | const extName: string = extname(filePath).replace('.', '').toLowerCase(); 104 | return await sharp(buffer, { animated: extName === 'gif' }) 105 | .toFormat( 106 | extName as keyof FormatEnum, 107 | options[ 108 | extName as keyof { 109 | png: PngOptions; 110 | jpeg: JpegOptions; 111 | jpg: JpegOptions; 112 | tiff: TiffOptions; 113 | gif: GifOptions; 114 | webp: WebpOptions; 115 | avif: AvifOptions; 116 | } 117 | ] 118 | ) 119 | .toBuffer(); 120 | }; 121 | 122 | const processFile = async (filePath: string, buffer: Buffer) => { 123 | try { 124 | let newBuffer: Buffer; 125 | 126 | let isCached: boolean; 127 | const cachedFilePath = join(options.cacheLocation ?? '', filePath); 128 | if (options.cache === true && fs.existsSync(cachedFilePath)) { 129 | // load buffer from cache (when enabled and available) 130 | newBuffer = await fsp.readFile(cachedFilePath); 131 | isCached = true; 132 | } else { 133 | // create buffer from engine 134 | const engine = /\.svg$/.test(filePath) ? applySVGO : applySharp; 135 | newBuffer = await engine(filePath, buffer); 136 | isCached = false; 137 | } 138 | 139 | // store buffer in cache 140 | if (options.cache === true && !isCached) { 141 | if (!fs.existsSync(dirname(cachedFilePath))) { 142 | await fsp.mkdir(dirname(cachedFilePath), { recursive: true }); 143 | } 144 | await fsp.writeFile(cachedFilePath, newBuffer); 145 | } 146 | 147 | // calculate sizes 148 | const newSize: number = newBuffer.byteLength; 149 | const oldSize: number = buffer.byteLength; 150 | const skipWrite: boolean = newSize >= oldSize; 151 | // save the sizes of the old and new image 152 | sizesMap.set(filePath, { 153 | size: newSize / 1024, 154 | oldSize: oldSize / 1024, 155 | ratio: Math.floor(100 * (newSize / oldSize - 1)), 156 | skipWrite, 157 | isCached, 158 | }); 159 | 160 | return { content: newBuffer, skipWrite }; 161 | } catch (error: any) { 162 | errorsMap.set(filePath, error.message); 163 | return {}; 164 | } 165 | }; 166 | 167 | const getFilesToProcess = (allFiles: string[], getFileName: Function) => { 168 | // include takes higher priority than `test` and `exclude` 169 | if (options.include) { 170 | return allFiles.reduce((acc, filePath) => { 171 | const fileName: string = getFileName(filePath); 172 | if (areFilesMatching(fileName, filePath, options.include)) { 173 | acc.push(filePath); 174 | } 175 | return acc; 176 | }, [] as string[]); 177 | } 178 | 179 | return allFiles.reduce((acc, filePath) => { 180 | if (options.test?.test(filePath)) { 181 | const fileName: string = getFileName(filePath); 182 | if (!areFilesMatching(fileName, filePath, options.exclude)) { 183 | acc.push(filePath); 184 | } 185 | } 186 | return acc; 187 | }, [] as string[]); 188 | }; 189 | 190 | const ensureCacheDirectoryExists = async function () { 191 | if (options.cache === true && options.cacheLocation && !fs.existsSync(options.cacheLocation)) { 192 | await fsp.mkdir(options.cacheLocation, { recursive: true }); 193 | } 194 | }; 195 | 196 | return { 197 | name: VITE_PLUGIN_NAME, 198 | enforce: 'post', 199 | apply: 'build', 200 | configResolved(c) { 201 | rootConfig = c; 202 | outputPath = c.build.outDir; 203 | if (typeof c.publicDir === 'string') { 204 | publicDir = c.publicDir.replace(/\\/g, '/'); 205 | } 206 | }, 207 | generateBundle: async (_, bundler) => { 208 | const allFiles: string[] = Object.keys(bundler); 209 | const files: string[] = getFilesToProcess(allFiles, (path: string) => (bundler[path] as any).name); 210 | 211 | if (files.length > 0) { 212 | await ensureCacheDirectoryExists(); 213 | 214 | const handles = files.map(async (filePath: string) => { 215 | const source = (bundler[filePath] as any).source; 216 | const { content, skipWrite } = await processFile(filePath, source); 217 | // write the file only if its optimized size < original size 218 | if (content && content.length > 0 && !skipWrite) { 219 | (bundler[filePath] as any).source = content; 220 | } 221 | }); 222 | await Promise.all(handles); 223 | } 224 | }, 225 | async closeBundle() { 226 | if (publicDir && options.includePublic) { 227 | // find static images in the original static folder 228 | const allFiles: string[] = readAllFiles(publicDir); 229 | const files: string[] = getFilesToProcess(allFiles, (path: string) => filename(path) + extname(path)); 230 | 231 | if (files.length > 0) { 232 | await ensureCacheDirectoryExists(); 233 | 234 | const handles = files.map(async (publicFilePath: string) => { 235 | // convert the path to the output folder 236 | const filePath: string = publicFilePath.replace(publicDir + sep, ''); 237 | const fullFilePath: string = resolve(rootConfig.root, outputPath, filePath); 238 | 239 | if (fs.existsSync(fullFilePath) === false) return; 240 | 241 | const { mtimeMs } = await fsp.stat(fullFilePath); 242 | if (mtimeMs <= (mtimeCache.get(filePath) || 0)) return; 243 | 244 | const buffer: Buffer = await fsp.readFile(fullFilePath); 245 | const { content, skipWrite } = await processFile(filePath, buffer); 246 | // write the file only if its optimized size < original size 247 | if (content && content?.length > 0 && !skipWrite) { 248 | await fsp.writeFile(fullFilePath, content); 249 | mtimeCache.set(filePath, Date.now()); 250 | } 251 | }); 252 | await Promise.all(handles); 253 | } 254 | } 255 | if (sizesMap.size > 0 && options.logStats) { 256 | logOptimizationStats(rootConfig, sizesMap, options.ansiColors ?? true); 257 | } 258 | if (errorsMap.size > 0) { 259 | logErrors(rootConfig, errorsMap, options.ansiColors ?? true); 260 | } 261 | }, 262 | }; 263 | } 264 | 265 | export { ViteImageOptimizer }; 266 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | vite logo 4 | 5 |

Vite Image Optimizer

6 |

7 | Plugin for Vite to optimize all images assets using Sharp.js and SVGO at build time. 8 |

9 | node-current 10 | npm peer dependency version 11 | npm bundle size 12 | GitHub release 13 | licence 14 |
15 | 16 |

 

17 | 18 | ## Features 19 | 20 | - Optimize SVG assets using [SVGO](https://github.com/svg/svgo) and pass custom configs. 21 | - Optimize Raster assets (png, jpeg, gif, tiff, webp, avif) using [Sharp.js](https://github.com/lovell/sharp) with the option to pass custom configs for each extension type. 22 | - Option to process all assets from your `public` directory defined in the bundler. 23 | - Configure `test`, `include`, and `exclude` to filter assets. 24 | - Caching support to avoid re-optimization (optional) 25 | - Skips processing assets if their optimized size is greater than their original size. 26 | - Log the optimization stats showing the before and after size difference, ratio and total savings (optional) 27 | ![terminal output image](https://images2.imgbox.com/6c/e7/DRpgWUM6_o.png) 28 | 29 | ## Motivation 30 | 31 | This plugin is based on the awesome [image-minimizer-webpack-plugin](https://github.com/webpack-contrib/image-minimizer-webpack-plugin) for Webpack. I wanted to combine the 32 | optimization capabilities of 33 | **Sharp.js** and **SVGO** in a single package and I couldn't find a plugin 34 | for Vite that could accomplish this. I initially thought of adding [squoosh](https://github.com/GoogleChromeLabs/squoosh) and [imagemin](https://github.com/imagemin/imagemin) support as well but 35 | dropped the idea since they are no 36 | longer 37 | maintained. 38 | 39 | If you find the plugin useful, consider showing your support by giving a ⭐ 40 | 41 | Contributions are most welcome! We follow [conventional-commits](https://www.conventionalcommits.org/en/v1.0.0/) 42 | 43 | ## Installation 44 | 45 | You can add it as a dev dependency to any of the package managers (NPM, Yarn, PNPM) 46 | 47 | Supports `Vite >=3` and `Node >=14` 48 | 49 | ```console 50 | npm install vite-plugin-image-optimizer --save-dev 51 | ``` 52 | 53 | > **Warning** 54 | > 55 | > `sharp` and `svgo` don't come installed as part of the package. You will have to install them manually and add it as a dev dependency. This is a design decision so you can choose to skip installing 56 | > `sharp` 57 | > if you only want to optimize svg assets using `svgo` and vice versa. 58 | > 59 | > ```console 60 | > npm install sharp --save-dev 61 | > ``` 62 | > 63 | > ```console 64 | > npm install svgo --save-dev 65 | > ``` 66 | 67 | ## Usage 68 | 69 | ```js 70 | import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'; 71 | import { defineConfig } from 'vite'; 72 | 73 | export default defineConfig(() => { 74 | return { 75 | plugins: [ 76 | ViteImageOptimizer({ 77 | /* pass your config */ 78 | }), 79 | ], 80 | }; 81 | }); 82 | ``` 83 | 84 | ## Default Configuration 85 | 86 | The default configuration is made for lossless compression of image assets. 87 | 88 | ```js 89 | const DEFAULT_OPTIONS = { 90 | logStats: true, 91 | ansiColors: true, 92 | test: /\.(jpe?g|png|gif|tiff|webp|svg|avif)$/i, 93 | exclude: undefined, 94 | include: undefined, 95 | includePublic: true, 96 | svg: { 97 | multipass: true, 98 | plugins: [ 99 | { 100 | name: 'preset-default', 101 | params: { 102 | overrides: { 103 | cleanupNumericValues: false, 104 | cleanupIds: { 105 | minify: false, 106 | remove: false, 107 | }, 108 | convertPathData: false, 109 | }, 110 | }, 111 | }, 112 | 'sortAttrs', 113 | { 114 | name: 'addAttributesToSVGElement', 115 | params: { 116 | attributes: [{ xmlns: 'http://www.w3.org/2000/svg' }], 117 | }, 118 | }, 119 | ], 120 | }, 121 | png: { 122 | // https://sharp.pixelplumbing.com/api-output#png 123 | quality: 100, 124 | }, 125 | jpeg: { 126 | // https://sharp.pixelplumbing.com/api-output#jpeg 127 | quality: 100, 128 | }, 129 | jpg: { 130 | // https://sharp.pixelplumbing.com/api-output#jpeg 131 | quality: 100, 132 | }, 133 | tiff: { 134 | // https://sharp.pixelplumbing.com/api-output#tiff 135 | quality: 100, 136 | }, 137 | // gif does not support lossless compression 138 | // https://sharp.pixelplumbing.com/api-output#gif 139 | gif: {}, 140 | webp: { 141 | // https://sharp.pixelplumbing.com/api-output#webp 142 | lossless: true, 143 | }, 144 | avif: { 145 | // https://sharp.pixelplumbing.com/api-output#avif 146 | lossless: true, 147 | }, 148 | cache: false, 149 | cacheLocation: undefined, 150 | }; 151 | ``` 152 | 153 | ## Plugin Options 154 | 155 | - **[`test`](#test)** 156 | - **[`include`](#include)** 157 | - **[`exclude`](#exclude)** 158 | - **[`includePublic`](#includepublic)** 159 | - **[`logStats`](#logstats)** 160 | - **[`ansiColors`](#ansiColors)** 161 | - **[`svg`](#svg)** 162 | - **[`png`](#png)** 163 | - **[`jpeg`](#jpeg)** 164 | - **[`tiff`](#tiff)** 165 | - **[`gif`](#gif)** 166 | - **[`webp`](#webp)** 167 | - **[`avif`](#webp)** 168 | - **[`cache`](#cache)** 169 | - **[`cacheLocation`](#cache)** 170 | 171 | ### `test` 172 | 173 | Type: `RegExp` 174 | 175 | Default: `/\.(jpe?g|png|gif|tiff|webp|svg|avif)$/i` 176 | 177 | Test to match files against. 178 | 179 | ### `exclude` 180 | 181 | Type: `String` | `RegExp` | `Array` 182 | 183 | Default: `undefined` 184 | 185 | Files to exclude. `RegExp` can be used on the source path to exclude a particular folder (`exclude: /textures/`). 186 | 187 | ### `include` 188 | 189 | Type: `String` | `RegExp` | `Array` 190 | 191 | Default: `undefined` 192 | 193 | Files to include. `RegExp` can be used on the source path to include a particular folder (`include: /textures/`). 194 | 195 | > **Warning** 196 | > 197 | > This will override any options set in `test` and `exclude` and has a higher preference. Use this option if you want to include specific assets only. 198 | 199 | ### `includePublic` 200 | 201 | Type: `boolean` 202 | 203 | Default: `true` 204 | 205 | Include all assets within the public directory defined in Vite. When `true` it will recursively traverse the directory and optimize all the assets. 206 | 207 | ### `logStats` 208 | 209 | Type: `boolean` 210 | 211 | Default: `true` 212 | 213 | Logs the optimization stats to terminal output with file size difference in kB, percent increase/decrease and total savings. 214 | 215 | ### `ansiColors` 216 | 217 | Type: `boolean` 218 | 219 | Default: `true` 220 | 221 | Logs the optimization stats or errors with ansi colors in the terminal. Set it to `false` for shells that don't support color text. 222 | 223 | ### `svg` 224 | 225 | Type: [`SVGOConfig`](https://github.com/svg/svgo/blob/main/lib/svgo.d.ts#L28) 226 | 227 | Default: 228 | 229 | ```js 230 | { 231 | multipass: true, 232 | plugins: [ 233 | { 234 | name: 'preset-default', 235 | params: { 236 | overrides: { 237 | cleanupNumericValues: false, 238 | removeViewBox: false, // https://github.com/svg/svgo/issues/1128 239 | }, 240 | cleanupIDs: { 241 | minify: false, 242 | remove: false, 243 | }, 244 | convertPathData: false, 245 | }, 246 | }, 247 | 'sortAttrs', 248 | { 249 | name: 'addAttributesToSVGElement', 250 | params: { 251 | attributes: [{ xmlns: 'http://www.w3.org/2000/svg' }], 252 | }, 253 | }, 254 | ] 255 | } 256 | ``` 257 | 258 | Config object to pass to SVGO, you can override it with your custom config. 259 | 260 | ### `png` 261 | 262 | Type: [`PngOptions`](https://github.com/lovell/sharp/blob/main/lib/index.d.ts#L1200) 263 | 264 | Default: 265 | 266 | ```js 267 | { 268 | // https://sharp.pixelplumbing.com/api-output#png 269 | quality: 100, 270 | } 271 | ``` 272 | 273 | Config object to pass to Sharp.js for assets with `png` extension 274 | 275 | ### `jpeg` 276 | 277 | Type: [`JpegOptions`](https://github.com/lovell/sharp/blob/main/lib/index.d.ts#L1060) 278 | 279 | Default: 280 | 281 | ```js 282 | { 283 | // https://sharp.pixelplumbing.com/api-output#jpeg 284 | quality: 100, 285 | } 286 | ``` 287 | 288 | Config object to pass to Sharp.js for assets with `jpg` or `jpeg` extension 289 | 290 | ### `gif` 291 | 292 | Type: [`GifOptions`](https://github.com/lovell/sharp/blob/main/lib/index.d.ts#L1156) 293 | 294 | Default: 295 | 296 | ```js 297 | { 298 | // https://sharp.pixelplumbing.com/api-output#gif 299 | } 300 | ``` 301 | 302 | Config object to pass to Sharp.js for assets with `gif` extension 303 | 304 | ### `tiff` 305 | 306 | Type: [`TiffOptions`](https://github.com/lovell/sharp/blob/main/lib/index.d.ts#L1175) 307 | 308 | Default: 309 | 310 | ```js 311 | { 312 | // https://sharp.pixelplumbing.com/api-output#tiff 313 | quality: 100, 314 | } 315 | ``` 316 | 317 | Config object to pass to Sharp.js for assets with `tiff` extension 318 | 319 | ### `webp` 320 | 321 | Type: [`WebpOptions`](https://github.com/lovell/sharp/blob/main/lib/index.d.ts#L1113) 322 | 323 | Default: 324 | 325 | ```js 326 | { 327 | // https://sharp.pixelplumbing.com/api-output#webp 328 | lossless: true, 329 | } 330 | ``` 331 | 332 | Config object to pass to Sharp.js for assets with `webp` extension 333 | 334 | ### `avif` 335 | 336 | Type: [`AvifOptions`](https://github.com/lovell/sharp/blob/main/lib/index.d.ts#L1132) 337 | 338 | Default: 339 | 340 | ```js 341 | { 342 | // https://sharp.pixelplumbing.com/api-output#avif 343 | lossless: true, 344 | } 345 | ``` 346 | 347 | Config object to pass to Sharp.js for assets with `avif` extension 348 | 349 | ### `cache` 350 | 351 | Type: `boolean` 352 | 353 | Default: `false` 354 | 355 | Cache assets in `cacheLocation`. When enabled, reads and writes asset files with their hash suffix from the specified path. 356 | 357 | ### `cacheLocation` 358 | 359 | Type: `String` 360 | 361 | Default: `undefined` 362 | 363 | Path to the cache directory. Can be used with GitHub Actions and other build servers that support cache directories to speed up consecutive builds. 364 | 365 | ## License 366 | 367 | [MIT](./LICENSE) 368 | --------------------------------------------------------------------------------