├── .gitattributes ├── .github ├── FUNDING.yml ├── renovate.json5 └── workflows │ ├── release-commit.yml │ ├── unit-test.yml │ └── release.yml ├── .vscode └── settings.json ├── tests ├── enums │ ├── txt.txt │ ├── ts.ts │ └── tsx.tsx ├── fixtures │ ├── main.ts │ └── mod.ts ├── __snapshots__ │ ├── vite.spec.ts.snap │ ├── rolldown.test.ts.snap │ ├── rollup.spec.ts.snap │ ├── esbuild.spec.ts.snap │ └── scan-enums.spec.ts.snap ├── esbuild.spec.ts ├── rollup.spec.ts ├── rolldown.test.ts ├── vite.spec.ts └── scan-enums.spec.ts ├── .gitignore ├── .editorconfig ├── tsdown.config.ts ├── eslint.config.js ├── src ├── api.ts ├── webpack.ts ├── vite.ts ├── rollup.ts ├── esbuild.ts ├── rolldown.ts ├── core │ ├── options.ts │ └── enum.ts └── index.ts ├── jsr.json ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: sxzz 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /tests/enums/txt.txt: -------------------------------------------------------------------------------- 1 | export enum TestEnum { 2 | A = 'b', 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.log 5 | .vercel 6 | .eslintcache 7 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: ['github>sxzz/renovate-config'], 3 | automerge: true, 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | end_of_line = lf 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /tests/enums/ts.ts: -------------------------------------------------------------------------------- 1 | export enum TestEnum { 2 | A = 'foo', 3 | B = 100, 4 | C = 1 << 2, 5 | D = 3.14, 6 | } 7 | -------------------------------------------------------------------------------- /tests/enums/tsx.tsx: -------------------------------------------------------------------------------- 1 | export enum TestEnum2 { 2 | A = 'foo', 3 | B = 100, 4 | C = 1 << 2, 5 | D = 3.14, 6 | } 7 | -------------------------------------------------------------------------------- /tests/fixtures/main.ts: -------------------------------------------------------------------------------- 1 | import { values, TestEnum } from './mod' 2 | 3 | console.log(TestEnum.A, TestEnum.B, TestEnum.C) 4 | console.log(values) 5 | -------------------------------------------------------------------------------- /tests/fixtures/mod.ts: -------------------------------------------------------------------------------- 1 | export const enum TestEnum { 2 | A = 'foo', 3 | B = 100, 4 | C = 1 << 2, 5 | } 6 | 7 | export const values = [TestEnum.A, TestEnum.B, TestEnum.C] 8 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | entry: './src/*.ts', 5 | exports: true, 6 | inlineOnly: [], 7 | }) 8 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { sxzz } from '@sxzz/eslint-config' 2 | 3 | export default sxzz().append({ 4 | files: ['README.md/*.ts'], 5 | rules: { 6 | 'import/no-mutable-exports': 'off', 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /tests/__snapshots__/vite.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`vite 1`] = ` 4 | "const values = ["foo", 100, 4]; 5 | console.log("foo", 100, 4); 6 | console.log(values); 7 | " 8 | `; 9 | -------------------------------------------------------------------------------- /.github/workflows/release-commit.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | on: [push, pull_request] 3 | 4 | permissions: {} 5 | 6 | jobs: 7 | release: 8 | uses: sxzz/workflows/.github/workflows/release-commit.yml@v1 9 | with: 10 | compact: true 11 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | unit-test: 13 | uses: sxzz/workflows/.github/workflows/unit-test.yml@v1 14 | -------------------------------------------------------------------------------- /tests/__snapshots__/rolldown.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`rolldown 1`] = ` 4 | "// main.js 5 | const values = [ 6 | "foo", 7 | 100, 8 | 4 9 | ]; 10 | console.log("foo", 100, 4); 11 | console.log(values); 12 | " 13 | `; 14 | -------------------------------------------------------------------------------- /tests/__snapshots__/rollup.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`rollup 1`] = ` 4 | "// main.js 5 | const values = [ 6 | "foo", 7 | 100, 8 | 4 9 | ]; 10 | 11 | console.log("foo", 100, 4); 12 | console.log(values); 13 | " 14 | `; 15 | -------------------------------------------------------------------------------- /tests/__snapshots__/esbuild.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`esbuild 1`] = ` 4 | "// tests/fixtures/mod.ts 5 | var values = ["foo", 100, 4]; 6 | 7 | // tests/fixtures/main.ts 8 | console.log("foo", 100, 4); 9 | console.log(values); 10 | " 11 | `; 12 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for exposing the core API. 3 | * 4 | * @module 5 | */ 6 | 7 | export { 8 | scanEnums, 9 | scanFiles, 10 | type EnumData, 11 | type EnumDeclaration, 12 | type EnumMember, 13 | type ScanOptions, 14 | } from './core/enum' 15 | 16 | export { resolveOptions, type Options } from './core/options' 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | uses: sxzz/workflows/.github/workflows/release.yml@v1 11 | with: 12 | publish: true 13 | permissions: 14 | contents: write 15 | id-token: write 16 | 17 | release-jsr: 18 | uses: sxzz/workflows/.github/workflows/release-jsr.yml@v1 19 | permissions: 20 | contents: read 21 | id-token: write 22 | -------------------------------------------------------------------------------- /src/webpack.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for webpack plugin. 3 | * 4 | * @module 5 | */ 6 | 7 | import { InlineEnum } from './index' 8 | 9 | /** 10 | * Webpack plugin 11 | * 12 | * @example 13 | * ```ts 14 | * // webpack.config.js 15 | * module.exports = { 16 | * plugins: [require('unplugin-inline-enum/webpack')()], 17 | * } 18 | * ``` 19 | */ 20 | const webpack = InlineEnum.webpack as typeof InlineEnum.webpack 21 | export default webpack 22 | export { webpack as 'module.exports' } 23 | -------------------------------------------------------------------------------- /src/vite.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for Vite plugin. 3 | * 4 | * @module 5 | */ 6 | 7 | import { InlineEnum } from './index' 8 | 9 | /** 10 | * Vite plugin 11 | * 12 | * @example 13 | * ```ts 14 | * // vite.config.ts 15 | * import InlineEnum from 'unplugin-inline-enum/vite' 16 | * 17 | * export default defineConfig({ 18 | * plugins: [InlineEnum()], 19 | * }) 20 | * ``` 21 | */ 22 | const vite = InlineEnum.vite as typeof InlineEnum.vite 23 | export default vite 24 | export { vite as 'module.exports' } 25 | -------------------------------------------------------------------------------- /src/rollup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for Rollup plugin. 3 | * 4 | * @module 5 | */ 6 | 7 | import { InlineEnum } from './index' 8 | 9 | /** 10 | * Rollup plugin 11 | * 12 | * @example 13 | * ```ts 14 | * // rollup.config.js 15 | * import InlineEnum from 'unplugin-inline-enum/rollup' 16 | * 17 | * export default { 18 | * plugins: [InlineEnum()], 19 | * } 20 | * ``` 21 | */ 22 | const rollup = InlineEnum.rollup as typeof InlineEnum.rollup 23 | export default rollup 24 | export { rollup as 'module.exports' } 25 | -------------------------------------------------------------------------------- /src/esbuild.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for esbuild plugin. 3 | * 4 | * @module 5 | */ 6 | 7 | import { InlineEnum } from './index' 8 | 9 | /** 10 | * Esbuild plugin 11 | * 12 | * @example 13 | * ```ts 14 | * // esbuild.config.js 15 | * import { build } from 'esbuild' 16 | * 17 | * build({ 18 | * plugins: [require('unplugin-inline-enum/esbuild')()], 19 | * }) 20 | * ``` 21 | */ 22 | const esbuild = InlineEnum.esbuild as typeof InlineEnum.esbuild 23 | export default esbuild 24 | export { esbuild as 'module.exports' } 25 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unplugin/inline-enum", 3 | "version": "0.6.3", 4 | "exports": { 5 | "./index": "./src/index.ts", 6 | "./api": "./src/api.ts", 7 | "./esbuild": "./src/esbuild.ts", 8 | "./rollup": "./src/rollup.ts", 9 | "./rolldown": "./src/rolldown.ts", 10 | "./vite": "./src/vite.ts", 11 | "./webpack": "./src/webpack.ts" 12 | }, 13 | "publish": { 14 | "include": [ 15 | "src", 16 | "package.json", 17 | "jsr.json", 18 | "README.md", 19 | "LICENSE" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/rolldown.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for Rolldown plugin. 3 | * 4 | * @module 5 | */ 6 | 7 | import { InlineEnum } from './index' 8 | 9 | /** 10 | * Rolldown plugin 11 | * 12 | * @example 13 | * ```ts 14 | * // rolldown.config.js 15 | * import InlineEnum from 'unplugin-inline-enum/rolldown' 16 | * 17 | * export default { 18 | * plugins: [InlineEnum()], 19 | * } 20 | * ``` 21 | */ 22 | const rolldown = InlineEnum.rolldown as typeof InlineEnum.rolldown 23 | export default rolldown 24 | export { rolldown as 'module.exports' } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["es2023"], 5 | "moduleDetection": "force", 6 | "module": "preserve", 7 | "moduleResolution": "bundler", 8 | "resolveJsonModule": true, 9 | "types": ["node"], 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "declaration": true, 13 | "esModuleInterop": true, 14 | "isolatedDeclarations": true, 15 | "isolatedModules": true, 16 | "verbatimModuleSyntax": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src", "tests"], 20 | "exclude": ["tests/fixtures"] 21 | } 22 | -------------------------------------------------------------------------------- /tests/esbuild.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { build } from 'esbuild' 3 | import { expect, test } from 'vitest' 4 | import UnpluginInlineEnum from '../src/esbuild' 5 | 6 | test('esbuild', async () => { 7 | const result = await build({ 8 | entryPoints: [path.resolve(__dirname, 'fixtures/main.ts')], 9 | bundle: true, 10 | write: false, 11 | format: 'esm', 12 | plugins: [ 13 | UnpluginInlineEnum({ 14 | scanDir: path.resolve(__dirname, 'fixtures'), 15 | scanMode: 'fs', 16 | }), 17 | ], 18 | }) 19 | expect(result.outputFiles[0].text).toMatchSnapshot() 20 | }) 21 | -------------------------------------------------------------------------------- /tests/rollup.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { rollupBuild } from '@sxzz/test-utils' 3 | import Oxc from 'unplugin-oxc/rollup' 4 | import { expect, test } from 'vitest' 5 | import UnpluginInlineEnum from '../src/rollup' 6 | 7 | test('rollup', async () => { 8 | const { snapshot } = await rollupBuild( 9 | path.resolve(__dirname, 'fixtures/main.ts'), 10 | [ 11 | UnpluginInlineEnum({ 12 | scanDir: path.resolve(__dirname, 'fixtures'), 13 | scanMode: 'fs', 14 | }), 15 | Oxc(), 16 | ], 17 | { 18 | treeshake: 'smallest', 19 | }, 20 | ) 21 | expect(snapshot).toMatchSnapshot() 22 | }) 23 | -------------------------------------------------------------------------------- /tests/rolldown.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { rolldownBuild } from '@sxzz/test-utils' 3 | import Oxc from 'unplugin-oxc/rolldown' 4 | import { expect, test } from 'vitest' 5 | import UnpluginInlineEnum from '../src/rolldown' 6 | 7 | test('rolldown', async () => { 8 | const { snapshot } = await rolldownBuild( 9 | path.resolve(__dirname, 'fixtures/main.ts'), 10 | [ 11 | UnpluginInlineEnum({ 12 | scanDir: path.resolve(__dirname, 'fixtures'), 13 | scanMode: 'fs', 14 | }), 15 | Oxc(), 16 | ], 17 | {}, 18 | { minify: 'dce-only' }, 19 | ) 20 | expect(snapshot).toMatchSnapshot() 21 | }) 22 | -------------------------------------------------------------------------------- /tests/vite.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { build } from 'vite' 3 | import { expect, test } from 'vitest' 4 | import UnpluginInlineEnum from '../src/vite' 5 | import type { RollupOutput } from 'rollup' 6 | 7 | test('vite', async () => { 8 | const root = path.resolve(__dirname, 'fixtures') 9 | const { output } = (await build({ 10 | root, 11 | build: { 12 | minify: false, 13 | rollupOptions: { 14 | input: [path.resolve(root, 'main.ts')], 15 | }, 16 | write: false, 17 | }, 18 | logLevel: 'silent', 19 | plugins: [ 20 | UnpluginInlineEnum({ 21 | scanDir: path.resolve(__dirname, 'fixtures'), 22 | scanMode: 'fs', 23 | }), 24 | ], 25 | })) as RollupOutput 26 | expect(output[0].code).toMatchSnapshot() 27 | }) 28 | -------------------------------------------------------------------------------- /tests/scan-enums.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { describe, expect, test } from 'vitest' 3 | import { scanEnums, type EnumData } from '../src/core/enum' 4 | import type { OptionsResolved } from '../src/core/options' 5 | 6 | describe('scanEnums', () => { 7 | const scanDir = path.resolve(__dirname, 'enums') 8 | const options: Omit = { 9 | include: [/\.ts/], 10 | exclude: [], 11 | scanDir: path.resolve(__dirname, 'enums'), 12 | scanPattern: ['**/*.ts', '**/*.tsx'], 13 | } 14 | 15 | let fsEnums: EnumData 16 | test('scanMode: fs', () => { 17 | fsEnums = scanEnums({ 18 | ...options, 19 | scanMode: 'fs', 20 | }) 21 | 22 | expect({ 23 | ...fsEnums, 24 | declarations: Object.fromEntries( 25 | Object.entries(fsEnums.declarations).map(([k, v]) => [ 26 | path.relative(scanDir, k), 27 | v, 28 | ]), 29 | ), 30 | }).toMatchSnapshot() 31 | }) 32 | 33 | test('scanMode: git', () => { 34 | const gitEnums = scanEnums({ 35 | ...options, 36 | scanMode: 'git', 37 | }) 38 | expect(gitEnums).toEqual(fsEnums) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2024-PRESENT Kevin Deng (https://github.com/sxzz) 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 | -------------------------------------------------------------------------------- /tests/__snapshots__/scan-enums.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`scanEnums > scanMode: fs 1`] = ` 4 | { 5 | "declarations": { 6 | "ts.ts": [ 7 | { 8 | "id": "TestEnum", 9 | "members": [ 10 | { 11 | "name": "A", 12 | "value": "foo", 13 | }, 14 | { 15 | "name": "B", 16 | "value": 100, 17 | }, 18 | { 19 | "name": "C", 20 | "value": 4, 21 | }, 22 | { 23 | "name": "D", 24 | "value": 3.14, 25 | }, 26 | ], 27 | "range": [ 28 | 0, 29 | 74, 30 | ], 31 | }, 32 | ], 33 | "tsx.tsx": [ 34 | { 35 | "id": "TestEnum2", 36 | "members": [ 37 | { 38 | "name": "A", 39 | "value": "foo", 40 | }, 41 | { 42 | "name": "B", 43 | "value": 100, 44 | }, 45 | { 46 | "name": "C", 47 | "value": 4, 48 | }, 49 | { 50 | "name": "D", 51 | "value": 3.14, 52 | }, 53 | ], 54 | "range": [ 55 | 0, 56 | 75, 57 | ], 58 | }, 59 | ], 60 | }, 61 | "defines": { 62 | "TestEnum.A": ""foo"", 63 | "TestEnum.B": "100", 64 | "TestEnum.C": "4", 65 | "TestEnum.D": "3.14", 66 | "TestEnum2.A": ""foo"", 67 | "TestEnum2.B": "100", 68 | "TestEnum2.C": "4", 69 | "TestEnum2.D": "3.14", 70 | }, 71 | } 72 | `; 73 | -------------------------------------------------------------------------------- /src/core/options.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import type { FilterPattern } from 'unplugin' 3 | 4 | /** 5 | * Represents the options for the plugin. 6 | */ 7 | export interface Options { 8 | include?: FilterPattern 9 | exclude?: FilterPattern 10 | enforce?: 'pre' | 'post' | undefined 11 | /** 12 | * The mode used to scan for enum files. 13 | * @default 'fs' 14 | */ 15 | scanMode?: 'git' | 'fs' 16 | /** 17 | * The directory to scan for enum files. 18 | * @default process.cwd() 19 | */ 20 | scanDir?: string 21 | /** 22 | * The pattern used to match enum files. 23 | * @default ['**\/*.{cts,mts,ts,tsx}', '!**\/node_modules'] 24 | */ 25 | scanPattern?: string | string[] 26 | } 27 | 28 | type Overwrite = Pick> & U 29 | 30 | /** 31 | * Represents the resolved options for the plugin. 32 | */ 33 | export type OptionsResolved = Overwrite< 34 | Required, 35 | Pick 36 | > 37 | 38 | /** 39 | * Resolves the options for the plugin. 40 | * @param options - The options to resolve. 41 | * @returns The resolved options. 42 | */ 43 | export function resolveOptions(options: Options): OptionsResolved { 44 | return { 45 | include: options.include || [/\.[cm]?[jt]sx?$/], 46 | exclude: options.exclude || [/node_modules/, /\.d\.[cm]?ts$/], 47 | enforce: 'enforce' in options ? options.enforce : 'pre', 48 | 49 | scanMode: options.scanMode || 'fs', 50 | scanDir: options.scanDir || process.cwd(), 51 | scanPattern: options.scanPattern || [ 52 | '**/*.{cts,mts,ts,tsx}', 53 | '!**/node_modules', 54 | ], 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unplugin-inline-enum", 3 | "type": "module", 4 | "version": "0.6.3", 5 | "packageManager": "pnpm@10.25.0", 6 | "description": "Inline enum values to optimize bundle size.", 7 | "author": "Kevin Deng ", 8 | "license": "MIT", 9 | "funding": "https://github.com/sponsors/sxzz", 10 | "homepage": "https://github.com/unplugin/unplugin-inline-enum#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/unplugin/unplugin-inline-enum.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/unplugin/unplugin-inline-enum/issues" 17 | }, 18 | "keywords": [ 19 | "unplugin", 20 | "rollup", 21 | "vite", 22 | "esbuild", 23 | "webpack" 24 | ], 25 | "exports": { 26 | ".": "./dist/index.mjs", 27 | "./api": "./dist/api.mjs", 28 | "./esbuild": "./dist/esbuild.mjs", 29 | "./rolldown": "./dist/rolldown.mjs", 30 | "./rollup": "./dist/rollup.mjs", 31 | "./vite": "./dist/vite.mjs", 32 | "./webpack": "./dist/webpack.mjs", 33 | "./package.json": "./package.json" 34 | }, 35 | "main": "./dist/index.mjs", 36 | "module": "./dist/index.mjs", 37 | "types": "./dist/index.d.mts", 38 | "typesVersions": { 39 | "*": { 40 | "*": [ 41 | "./dist/*.d.mts", 42 | "./*" 43 | ] 44 | } 45 | }, 46 | "files": [ 47 | "dist" 48 | ], 49 | "publishConfig": { 50 | "access": "public" 51 | }, 52 | "engines": { 53 | "node": ">=20.19.0" 54 | }, 55 | "scripts": { 56 | "lint": "eslint --cache .", 57 | "lint:fix": "pnpm run lint --fix", 58 | "build": "tsdown", 59 | "dev": "tsdown --watch", 60 | "test": "vitest", 61 | "typecheck": "tsgo --noEmit", 62 | "release": "bumpp", 63 | "prepublishOnly": "pnpm run build" 64 | }, 65 | "dependencies": { 66 | "ast-kit": "^2.2.0", 67 | "magic-string": "^0.30.21", 68 | "picomatch": "^4.0.3", 69 | "tinyglobby": "^0.2.15", 70 | "unplugin": "^2.3.11", 71 | "unplugin-replace": "^0.6.2" 72 | }, 73 | "devDependencies": { 74 | "@babel/types": "^7.28.5", 75 | "@sxzz/eslint-config": "^7.4.3", 76 | "@sxzz/prettier-config": "^2.2.6", 77 | "@sxzz/test-utils": "^0.5.14", 78 | "@types/node": "^25.0.2", 79 | "@types/picomatch": "^4.0.2", 80 | "@typescript/native-preview": "7.0.0-dev.20251214.1", 81 | "bumpp": "^10.3.2", 82 | "esbuild": "^0.27.1", 83 | "eslint": "^9.39.2", 84 | "prettier": "^3.7.4", 85 | "rollup": "^4.53.3", 86 | "tsdown": "^0.17.4", 87 | "typescript": "^5.9.3", 88 | "unplugin-oxc": "^0.5.6", 89 | "vite": "^7.2.7", 90 | "vitest": "^4.0.15", 91 | "webpack": "^5.103.0" 92 | }, 93 | "prettier": "@sxzz/prettier-config" 94 | } 95 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for main InlineEnum. 3 | * @module 4 | */ 5 | 6 | import MagicString from 'magic-string' 7 | import { createUnplugin, type UnpluginInstance } from 'unplugin' 8 | import ReplacePlugin from 'unplugin-replace' 9 | import { scanEnums } from './core/enum' 10 | import { resolveOptions, type Options } from './core/options' 11 | 12 | /** 13 | * The main unplugin instance. 14 | */ 15 | const InlineEnum: UnpluginInstance = createUnplugin< 16 | Options | undefined, 17 | true 18 | >((rawOptions = {}, meta) => { 19 | const options = resolveOptions(rawOptions) 20 | const { declarations, defines } = scanEnums(options) 21 | 22 | const replacePlugin = Object.assign( 23 | ReplacePlugin.raw( 24 | { 25 | include: options.include, 26 | exclude: options.exclude, 27 | values: defines, 28 | }, 29 | meta, 30 | ), 31 | { name: 'unplugin-inline-enum:replace' }, 32 | ) 33 | 34 | const name = 'unplugin-inline-enum' 35 | return [ 36 | replacePlugin, 37 | { 38 | name, 39 | enforce: options.enforce, 40 | 41 | transform: { 42 | filter: { 43 | id: { 44 | include: options.include, 45 | exclude: options.exclude, 46 | }, 47 | }, 48 | handler(code, id) { 49 | if (!(id in declarations)) return 50 | 51 | const s: MagicString = new MagicString(code) 52 | for (const declaration of declarations[id]) { 53 | const { 54 | range: [start, end], 55 | id, 56 | members, 57 | } = declaration 58 | s.update( 59 | start, 60 | end, 61 | `export const ${id} = {${members 62 | .flatMap(({ name, value }) => { 63 | const forwardMapping = `${JSON.stringify(name)}: ${JSON.stringify(value)}` 64 | const reverseMapping = `${JSON.stringify(value.toString())}: ${JSON.stringify(name)}` 65 | 66 | // see https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings 67 | return typeof value === 'string' 68 | ? [ 69 | forwardMapping, 70 | // string enum members do not get a reverse mapping generated at all 71 | ] 72 | : [ 73 | forwardMapping, 74 | // other enum members should support enum reverse mapping 75 | reverseMapping, 76 | ] 77 | }) 78 | .join(',\n')}}`, 79 | ) 80 | } 81 | 82 | if (s.hasChanged()) { 83 | return { 84 | code: s.toString(), 85 | get map() { 86 | return s.generateMap({ 87 | hires: 'boundary', 88 | source: id, 89 | includeContent: true, 90 | }) 91 | }, 92 | } 93 | } 94 | }, 95 | }, 96 | }, 97 | ] 98 | }) 99 | 100 | export { InlineEnum, resolveOptions, type Options } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unplugin-inline-enum 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![jsr][jsr-src]][jsr-href] 6 | [![Unit Test][unit-test-src]][unit-test-href] 7 | 8 | Inline enum values to optimize bundle size. 9 | 10 | ## Features 11 | 12 | - 🚀 Inline enum values to reduce the size of the bundle. 13 | - 🧹 Simplify generated enums in JavaScript. 14 | 15 | ```ts 16 | export enum TestEnum { 17 | a = 1, 18 | b = 'foo', 19 | } 20 | console.log(TestEnum.a, TestEnum.b) 21 | 22 | // before 23 | export let TestEnum 24 | ;(function (TestEnum) { 25 | TestEnum[(TestEnum.a = 1)] = 'a' 26 | TestEnum.b = 'foo' 27 | })(TestEnum || (TestEnum = {})) 28 | 29 | console.log(TestEnum.a, TestEnum.b) 30 | 31 | // after 32 | const TestEnum = { 33 | a: 1, 34 | '1': 'a', 35 | b: 'foo', 36 | } 37 | console.log(1, 'foo') 38 | ``` 39 | 40 | ## Installation 41 | 42 | ```bash 43 | # npm 44 | npm i -D unplugin-inline-enum 45 | 46 | # jsr 47 | npx jsr add -D @unplugin/inline-enum 48 | ``` 49 | 50 |
51 | Vite
52 | 53 | ```ts 54 | // vite.config.ts 55 | import InlineEnum from 'unplugin-inline-enum/vite' 56 | 57 | export default defineConfig({ 58 | plugins: [InlineEnum()], 59 | }) 60 | ``` 61 | 62 |
63 | 64 |
65 | Rollup
66 | 67 | ```ts 68 | // rollup.config.js 69 | import InlineEnum from 'unplugin-inline-enum/rollup' 70 | 71 | export default { 72 | plugins: [InlineEnum()], 73 | } 74 | ``` 75 | 76 |
77 | 78 |
79 | Rolldown
80 | 81 | ```ts 82 | // rolldown.config.js 83 | import InlineEnum from 'unplugin-inline-enum/rolldown' 84 | 85 | export default { 86 | plugins: [InlineEnum()], 87 | } 88 | ``` 89 | 90 |
91 | 92 |
93 | esbuild
94 | 95 | ```ts 96 | // esbuild.config.js 97 | import { build } from 'esbuild' 98 | 99 | build({ 100 | plugins: [require('unplugin-inline-enum/esbuild')()], 101 | }) 102 | ``` 103 | 104 |
105 | 106 |
107 | Webpack
108 | 109 | ```ts 110 | // webpack.config.js 111 | module.exports = { 112 | /* ... */ 113 | plugins: [require('unplugin-inline-enum/webpack')()], 114 | } 115 | ``` 116 | 117 |
118 | 119 | ## Options 120 | 121 | Refer to [docs](https://jsr.io/@unplugin/inline-enum/doc/api/~/Options). 122 | 123 | ## Credits 124 | 125 | Thanks to [@xiaoxiangmoe](https://github.com/xiaoxiangmoe) and 126 | [@yangmingshan](https://github.com/yangmingshan) for their contributions in the 127 | [PR](https://github.com/vuejs/core/pull/9261). 128 | 129 | ## Sponsors 130 | 131 |

132 | 133 | 134 | 135 |

136 | 137 | ## License 138 | 139 | [MIT](./LICENSE) License © 2024-PRESENT [Kevin Deng](https://github.com/sxzz) 140 | 141 | 142 | 143 | [npm-version-src]: https://img.shields.io/npm/v/unplugin-inline-enum.svg 144 | [npm-version-href]: https://npmjs.com/package/unplugin-inline-enum 145 | [npm-downloads-src]: https://img.shields.io/npm/dm/unplugin-inline-enum 146 | [npm-downloads-href]: https://www.npmcharts.com/compare/unplugin-inline-enum?interval=30 147 | [jsr-src]: https://jsr.io/badges/@unplugin/inline-enum 148 | [jsr-href]: https://jsr.io/@unplugin/inline-enum 149 | [unit-test-src]: https://github.com/unplugin/unplugin-inline-enum/actions/workflows/unit-test.yml/badge.svg 150 | [unit-test-href]: https://github.com/unplugin/unplugin-inline-enum/actions/workflows/unit-test.yml 151 | -------------------------------------------------------------------------------- /src/core/enum.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import { spawnSync } from 'node:child_process' 3 | import { readFileSync } from 'node:fs' 4 | import path from 'node:path' 5 | import { babelParse, getLang, isDts, isTs } from 'ast-kit' 6 | import picomatch from 'picomatch' 7 | import { globSync } from 'tinyglobby' 8 | import type { OptionsResolved } from './options' 9 | import type { Expression, PrivateName } from '@babel/types' 10 | 11 | /** 12 | * Represents the scan options for the enum. 13 | */ 14 | export type ScanOptions = Pick< 15 | OptionsResolved, 16 | 'scanDir' | 'scanMode' | 'scanPattern' 17 | > 18 | 19 | /** 20 | * Represents a member of an enum. 21 | */ 22 | export interface EnumMember { 23 | readonly name: string 24 | readonly value: string | number 25 | } 26 | 27 | /** 28 | * Represents a declaration of an enum. 29 | */ 30 | export interface EnumDeclaration { 31 | readonly id: string 32 | readonly range: readonly [start: number, end: number] 33 | readonly members: ReadonlyArray 34 | } 35 | 36 | /** 37 | * Represents the data of all enums. 38 | */ 39 | export interface EnumData { 40 | readonly declarations: { 41 | readonly [file: string]: ReadonlyArray 42 | } 43 | readonly defines: { readonly [id_key: `${string}.${string}`]: string } 44 | } 45 | 46 | /** 47 | * Evaluates a JavaScript expression and returns the result. 48 | * @param exp - The expression to evaluate. 49 | * @returns The evaluated result. 50 | */ 51 | function evaluate(exp: string): string | number { 52 | return new Function(`return ${exp}`)() 53 | } 54 | 55 | /** 56 | * Scans the specified directory for enums based on the provided options. 57 | * @param options - The scan options for the enum. 58 | * @returns The data of all enums found. 59 | */ 60 | export function scanEnums(options: ScanOptions): EnumData { 61 | const declarations: { [file: string]: EnumDeclaration[] } = 62 | Object.create(null) 63 | 64 | const defines: { [id_key: `${string}.${string}`]: string } = 65 | Object.create(null) 66 | 67 | // 1. grep for files with exported enum 68 | const files = scanFiles(options) 69 | 70 | // 2. parse matched files to collect enum info 71 | for (const file of files) { 72 | const lang = getLang(file) 73 | if (!isTs(lang) || isDts(file)) continue 74 | 75 | const content = readFileSync(file, 'utf8') 76 | const ast = babelParse(content, lang) 77 | 78 | const enumIds: Set = new Set() 79 | for (const node of ast.body) { 80 | if ( 81 | node.type === 'ExportNamedDeclaration' && 82 | node.declaration && 83 | node.declaration.type === 'TSEnumDeclaration' 84 | ) { 85 | const decl = node.declaration 86 | const id = decl.id.name 87 | if (enumIds.has(id)) { 88 | throw new Error( 89 | `not support declaration merging for enum ${id} in ${file}`, 90 | ) 91 | } 92 | enumIds.add(id) 93 | 94 | let lastInitialized: string | number | undefined 95 | const members: Array = [] 96 | 97 | for (const e of decl.members) { 98 | const key = e.id.type === 'Identifier' ? e.id.name : e.id.value 99 | const fullKey = `${id}.${key}` as const 100 | const saveValue = (value: string | number) => { 101 | // We need allow same name enum in different file. 102 | // For example: enum ErrorCodes exist in both @vue/compiler-core and @vue/runtime-core 103 | // But not allow `ErrorCodes.__EXTEND_POINT__` appear in two same name enum 104 | if (fullKey in defines) { 105 | throw new Error(`name conflict for enum ${id} in ${file}`) 106 | } 107 | members.push({ 108 | name: key, 109 | value, 110 | }) 111 | defines[fullKey] = JSON.stringify(value) 112 | } 113 | const init = e.initializer 114 | if (init) { 115 | let value: string | number 116 | switch (init.type) { 117 | case 'StringLiteral': 118 | case 'NumericLiteral': { 119 | value = init.value 120 | 121 | break 122 | } 123 | case 'BinaryExpression': { 124 | const resolveValue = (node: Expression | PrivateName) => { 125 | assert.ok(typeof node.start === 'number') 126 | assert.ok(typeof node.end === 'number') 127 | if ( 128 | node.type === 'NumericLiteral' || 129 | node.type === 'StringLiteral' 130 | ) { 131 | return node.value 132 | } else if (node.type === 'MemberExpression') { 133 | const exp = content.slice( 134 | node.start, 135 | node.end, 136 | ) as `${string}.${string}` 137 | if (!(exp in defines)) { 138 | throw new Error( 139 | `unhandled enum initialization expression ${exp} in ${file}`, 140 | ) 141 | } 142 | return defines[exp] 143 | } else { 144 | throw new Error( 145 | `unhandled BinaryExpression operand type ${node.type} in ${file}`, 146 | ) 147 | } 148 | } 149 | const exp = `${resolveValue(init.left)}${ 150 | init.operator 151 | }${resolveValue(init.right)}` 152 | value = evaluate(exp) 153 | 154 | break 155 | } 156 | case 'UnaryExpression': { 157 | if ( 158 | init.argument.type === 'StringLiteral' || 159 | init.argument.type === 'NumericLiteral' 160 | ) { 161 | const exp = `${init.operator}${init.argument.value}` 162 | value = evaluate(exp) 163 | } else { 164 | throw new Error( 165 | `unhandled UnaryExpression argument type ${init.argument.type} in ${file}`, 166 | ) 167 | } 168 | 169 | break 170 | } 171 | default: { 172 | throw new Error( 173 | `unhandled initializer type ${init.type} for ${fullKey} in ${file}`, 174 | ) 175 | } 176 | } 177 | lastInitialized = value 178 | saveValue(lastInitialized) 179 | } else if (lastInitialized === undefined) { 180 | // first initialized 181 | lastInitialized = 0 182 | saveValue(lastInitialized) 183 | } else if (typeof lastInitialized === 'number') { 184 | lastInitialized++ 185 | saveValue(lastInitialized) 186 | } else { 187 | // should not happen 188 | throw new TypeError(`wrong enum initialization sequence in ${file}`) 189 | } 190 | } 191 | 192 | if (!(file in declarations)) { 193 | declarations[file] = [] 194 | } 195 | assert.ok(typeof node.start === 'number') 196 | assert.ok(typeof node.end === 'number') 197 | declarations[file].push({ 198 | id, 199 | range: [node.start, node.end], 200 | members, 201 | }) 202 | } 203 | } 204 | } 205 | 206 | const enumData: EnumData = { 207 | declarations, 208 | defines, 209 | } 210 | return enumData 211 | } 212 | 213 | /** 214 | * Scans the specified directory for files based on the provided options. 215 | * @param options - The scan options for the files. 216 | * @returns The list of files found. 217 | */ 218 | export function scanFiles(options: ScanOptions): string[] { 219 | if (options.scanMode === 'fs') { 220 | return globSync(options.scanPattern, { 221 | cwd: options.scanDir, 222 | expandDirectories: false, 223 | }).map((file) => path.resolve(options.scanDir, file)) 224 | } else { 225 | const { stdout, stderr, status } = spawnSync( 226 | 'git', 227 | ['grep', '--untracked', 'export enum'], 228 | { cwd: options.scanDir, encoding: 'utf8' }, 229 | ) 230 | if (status !== 0) { 231 | if (stderr) throw new Error(`git grep failed: ${stderr}`) 232 | else return [] 233 | } 234 | 235 | const matcher = picomatch(options.scanPattern) 236 | return [...new Set(stdout.split('\n').map((line) => line.split(':')[0]))] 237 | .map((file) => path.resolve(options.scanDir, file)) 238 | .filter((file) => matcher(file)) 239 | } 240 | } 241 | --------------------------------------------------------------------------------