├── examples ├── rspack-vue3 │ ├── public │ │ └── .gitkeep │ ├── .vscode │ │ └── extensions.json │ ├── src │ │ ├── index.ts │ │ ├── index.css │ │ ├── env.d.ts │ │ └── App.vue │ ├── .gitignore │ ├── rsbuild.config.ts │ ├── package.json │ └── tsconfig.json ├── vite-react │ ├── src │ │ ├── vite-env.d.ts │ │ ├── main.tsx │ │ ├── index.css │ │ ├── App.css │ │ ├── App.tsx │ │ ├── favicon.svg │ │ └── logo.svg │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── index.html │ ├── package.json │ └── tsconfig.json ├── vue-cli-vue2 │ ├── babel.config.js │ ├── src │ │ ├── index.css │ │ ├── main.ts │ │ └── App.vue │ ├── public │ │ └── index.html │ ├── package.json │ └── vue.config.js ├── vite-vue3 │ ├── public │ │ └── favicon.ico │ ├── global.d.ts │ ├── tsconfig.node.json │ ├── src │ │ ├── main.ts │ │ ├── env.d.ts │ │ ├── utils │ │ │ └── auto-decimal.ts │ │ ├── components │ │ │ └── test.vue │ │ ├── types │ │ │ └── shims-axios.d.ts │ │ └── App.vue │ ├── auto-decimal.d.ts │ ├── index.html │ ├── tsconfig.json │ ├── package.json │ └── vite.config.ts ├── vue-cli-vue3 │ ├── src │ │ ├── main.js │ │ ├── components │ │ │ ├── test.vue │ │ │ └── test-jsx.vue │ │ └── App.vue │ ├── babel.config.js │ ├── vue.config.js │ ├── jsconfig.json │ ├── public │ │ └── index.html │ └── package.json └── vite-vue2 │ ├── src │ ├── main.js │ └── components │ │ ├── test.vue │ │ └── test-jsx.jsx │ ├── style.css │ ├── vite.config.js │ ├── index.html │ ├── package.json │ ├── App.vue │ └── favicon.svg ├── .npmignore ├── src ├── index.ts ├── farm.ts ├── vite.ts ├── rollup.ts ├── rspack.ts ├── esbuild.ts ├── webpack.ts ├── core │ ├── traverse │ │ ├── index.ts │ │ ├── import-declaration.ts │ │ ├── export-declaration.ts │ │ ├── ast.ts │ │ ├── comment.ts │ │ ├── call-expression.ts │ │ ├── binary-expression.ts │ │ └── new-function.ts │ ├── generate.ts │ ├── unplugin.ts │ ├── constant.ts │ ├── options.ts │ ├── utils.ts │ └── transform.ts ├── astro.ts ├── nuxt.ts └── types.ts ├── .npmrc ├── pnpm-workspace.yaml ├── playground ├── main.ts ├── vite.config.ts ├── package.json └── index.html ├── docs ├── .vitepress │ ├── theme │ │ ├── index.ts │ │ └── style.css │ ├── plugins │ │ └── tag.ts │ ├── config.mts │ └── assets │ │ └── rspack.svg ├── guide │ ├── comment │ │ ├── index.md │ │ ├── splicing.md │ │ └── ad-ignore.md │ ├── api │ │ ├── index.md │ │ ├── support-string.md │ │ ├── tail-patch-zero.md │ │ ├── new-function.md │ │ └── to-decimal.md │ ├── what-is-auto-decimal.md │ └── getting-started.md ├── index.md └── public │ ├── logo.svg │ └── favicon.svg ├── vitest.config.ts ├── tsup.config.ts ├── eslint.config.js ├── tsconfig.json ├── .vscode ├── launch.json └── settings.json ├── .github └── workflows │ ├── changelog.yml │ ├── release.yml │ └── gh-pages.yml ├── test ├── fixtures │ ├── test.tsx │ ├── to-decimal.ts │ ├── new-function-to-decimal.ts │ ├── test.ts │ ├── new-function.ts │ ├── new-function-inject-window.ts │ ├── options.vue │ └── setup.vue ├── tsx.test.ts ├── to-decimal.test.ts ├── new-function-to-decimal.test.ts ├── new-function.test.ts ├── new-function-inject-window.test.ts ├── setup.test.ts ├── options.test.ts └── ts.test.ts ├── README_NPM.md ├── scripts └── switch-readme.cjs ├── README.md ├── LICENSE ├── .gitignore └── package.json /examples/rspack-vue3/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | README_NPM.md 2 | README.md.backup 3 | scripts/* -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './core/unplugin' 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - examples/* 4 | -------------------------------------------------------------------------------- /examples/vite-react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playground/main.ts: -------------------------------------------------------------------------------- 1 | document.getElementById('app')!.innerHTML = (0.1 + 0.2).toString() 2 | -------------------------------------------------------------------------------- /examples/rspack-vue3/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/vue-cli-vue2/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset', 4 | ], 5 | } 6 | -------------------------------------------------------------------------------- /examples/vite-vue3/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyumg/unplugin-auto-decimal/HEAD/examples/vite-vue3/public/favicon.ico -------------------------------------------------------------------------------- /examples/vue-cli-vue3/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | console.log(0.1 + 0.2, '0.2') 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /examples/rspack-vue3/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import './index.css'; 4 | 5 | createApp(App).mount('#root'); 6 | -------------------------------------------------------------------------------- /examples/vite-vue2/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./../App.vue" 3 | 4 | new Vue({ 5 | el: "#app", 6 | render: (h) => h(App) 7 | }).$mount(); -------------------------------------------------------------------------------- /examples/vue-cli-vue3/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset', 4 | ], 5 | plugins: ["@vue/babel-plugin-jsx"] 6 | } 7 | -------------------------------------------------------------------------------- /src/farm.ts: -------------------------------------------------------------------------------- 1 | import type { AutoDecimalOptions } from './types' 2 | import unplugin from '.' 3 | 4 | export default unplugin.farm as (options?: AutoDecimalOptions) => any 5 | -------------------------------------------------------------------------------- /src/vite.ts: -------------------------------------------------------------------------------- 1 | import type { AutoDecimalOptions } from './types' 2 | import unplugin from '.' 3 | 4 | export default unplugin.vite as (options?: AutoDecimalOptions) => any 5 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import 'virtual:group-icons.css' 3 | import './style.css' 4 | 5 | export default DefaultTheme 6 | -------------------------------------------------------------------------------- /src/rollup.ts: -------------------------------------------------------------------------------- 1 | import type { AutoDecimalOptions } from './types' 2 | import unplugin from '.' 3 | 4 | export default unplugin.rollup as (options?: AutoDecimalOptions) => any 5 | -------------------------------------------------------------------------------- /src/rspack.ts: -------------------------------------------------------------------------------- 1 | import type { AutoDecimalOptions } from './types' 2 | import unplugin from '.' 3 | 4 | export default unplugin.rspack as (options?: AutoDecimalOptions) => any 5 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['test/**/*.test.ts'], 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /src/esbuild.ts: -------------------------------------------------------------------------------- 1 | import type { AutoDecimalOptions } from './types' 2 | import unplugin from '.' 3 | 4 | export default unplugin.esbuild as (options?: AutoDecimalOptions) => any 5 | -------------------------------------------------------------------------------- /src/webpack.ts: -------------------------------------------------------------------------------- 1 | import type { AutoDecimalOptions } from './types' 2 | import unplugin from '.' 3 | 4 | export default unplugin.webpack as (options?: AutoDecimalOptions) => any 5 | -------------------------------------------------------------------------------- /examples/vite-vue3/global.d.ts: -------------------------------------------------------------------------------- 1 | export { } 2 | 3 | declare module 'unplugin-auto-decimal/types' { 4 | interface AutoDecimal{ 5 | decimal: import('decimal.js-light').Decimal 6 | } 7 | } -------------------------------------------------------------------------------- /examples/rspack-vue3/.gitignore: -------------------------------------------------------------------------------- 1 | # Local 2 | .DS_Store 3 | *.local 4 | *.log* 5 | 6 | # Dist 7 | node_modules 8 | dist/ 9 | 10 | # IDE 11 | .vscode/* 12 | !.vscode/extensions.json 13 | .idea 14 | -------------------------------------------------------------------------------- /examples/vite-react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/rspack-vue3/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | color: #fff; 4 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 5 | background-image: linear-gradient(to bottom, #020917, #101725); 6 | } 7 | -------------------------------------------------------------------------------- /examples/vite-vue3/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/vue-cli-vue2/src/index.css: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: Avenir, Helvetica, Arial, sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | color: #2c3e50; 6 | margin-top: 30px; 7 | } 8 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup' 2 | 3 | export default { 4 | entry: ['src/*.ts'], 5 | clean: true, 6 | format: ['cjs', 'esm'], 7 | dts: true, 8 | cjsInterop: true, 9 | splitting: true, 10 | } 11 | -------------------------------------------------------------------------------- /examples/vite-vue2/style.css: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: Avenir, Helvetica, Arial, sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | text-align: center; 6 | color: #2c3e50; 7 | margin-top: 60px; 8 | } 9 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import Inspect from 'vite-plugin-inspect' 3 | import Unplugin from '../src/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | Inspect(), 8 | Unplugin(), 9 | ], 10 | }) 11 | -------------------------------------------------------------------------------- /src/core/traverse/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ast' 2 | export * from './binary-expression' 3 | export * from './call-expression' 4 | export * from './comment' 5 | export * from './export-declaration' 6 | export * from './import-declaration' 7 | export * from './new-function' 8 | -------------------------------------------------------------------------------- /examples/vite-react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import autoDecimal from 'unplugin-auto-decimal/vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [autoDecimal(), react()], 8 | }) 9 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | type: 'lib', 5 | ignores: ['**/*.md', 'examples/*'], 6 | rules: { 7 | 'ts/explicit-function-return-type': 'off', 8 | 'unicorn/consistent-function-scoping': 'off', 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /examples/vite-react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ) 12 | -------------------------------------------------------------------------------- /examples/vue-cli-vue2/src/main.ts: -------------------------------------------------------------------------------- 1 | // import VueCompostionAPI from '@vue/composition-api' 2 | import Vue from 'vue' 3 | import App from './App.vue' 4 | import './index.css' 5 | 6 | Vue.config.productionTip = false 7 | 8 | const app = new Vue({ render: h => h(App ) }) 9 | 10 | app.$mount('#app') 11 | -------------------------------------------------------------------------------- /docs/guide/comment/index.md: -------------------------------------------------------------------------------- 1 | # 跳过转换 2 | 3 | 有的时候,有些计算是不需要转换的。那么要如何跳过某个计算表达式或者都跳过呢? 4 | 5 | - 添加相应的注释(`jsx` 中需要注意, 在表达式中一些情况下是需要使用 JavaScript 注释) 6 | - `supportString: true`时, 末尾拼接一个空字符串 7 | - `supportString: false`时, 末尾拼接任意字符串 8 | 9 | 10 | #### [末位拼接空字符串](splicing.md) 11 | 12 | 13 | #### [添加相应的注释](ad-ignore.md) -------------------------------------------------------------------------------- /examples/vite-vue3/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | // 创建vue实例 5 | const app = createApp(App) 6 | // console.log(0.1 + 0.2, 'main') 7 | // 挂载实例 8 | app.mount('#app'); 9 | 10 | const result = (0.1 + 0.2).toDecimal({ cm: 'decimal'}) 11 | console.log(result.eq(0.3)) -------------------------------------------------------------------------------- /examples/rspack-vue3/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | 6 | // biome-ignore lint/complexity/noBannedTypes: reason 7 | const component: DefineComponent<{}, {}, any>; 8 | export default component; 9 | } 10 | -------------------------------------------------------------------------------- /examples/vite-vue3/auto-decimal.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-auto-decimal 4 | type ToDecimal = import('unplugin-auto-decimal/types').ToDecimal 5 | declare interface String { 6 | toDecimal: ToDecimal 7 | } 8 | declare interface Number { 9 | toDecimal: ToDecimal 10 | } 11 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "nodemon -w '../src/**/*.ts' -e .ts -x vite" 5 | }, 6 | "dependencies": { 7 | "decimal.js-light": "^2.5.1" 8 | }, 9 | "devDependencies": { 10 | "vite": "^5.4.2", 11 | "vite-plugin-inspect": "^0.8.7" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/vue-cli-vue3/src/components/test.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /examples/vite-vue3/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | -------------------------------------------------------------------------------- /examples/vue-cli-vue3/vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('@vue/cli-service') 2 | const AutoDecimal = require('unplugin-auto-decimal/webpack') 3 | 4 | module.exports = defineConfig({ 5 | transpileDependencies: true, 6 | configureWebpack: { 7 | devtool: 'source-map', 8 | plugins: [ 9 | AutoDecimal(), 10 | ], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/vite-vue2/src/components/test.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /examples/rspack-vue3/rsbuild.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rsbuild/core'; 2 | import { pluginVue } from '@rsbuild/plugin-vue'; 3 | import AutoDecimal from 'unplugin-auto-decimal/rspack' 4 | export default defineConfig({ 5 | plugins: [pluginVue()], 6 | tools: { 7 | rspack: { 8 | plugins: [AutoDecimal({tailPatchZero: true})] 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["esnext", "DOM"], 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "esModuleInterop": true 11 | }, 12 | "exclude": ["dist", "eslint.config.js", "examples", "playground"] 13 | } 14 | -------------------------------------------------------------------------------- /examples/vue-cli-vue3/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "dom.iterable", 16 | "scripthost" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/vite-vue2/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue2' 3 | import vueJsx from '@vitejs/plugin-vue2-jsx' 4 | import AutoDecimal from 'unplugin-auto-decimal/vite' 5 | import Inspect from 'vite-plugin-inspect' 6 | 7 | export default defineConfig({ 8 | plugins: [vue(), vueJsx(), AutoDecimal({ tailPatchZero: true, dts: false}), Inspect()], 9 | }) 10 | -------------------------------------------------------------------------------- /src/astro.ts: -------------------------------------------------------------------------------- 1 | import type { AutoDecimalOptions } from './types' 2 | 3 | import unplugin from '.' 4 | 5 | export default (options: AutoDecimalOptions): any => ({ 6 | name: 'unplugin-auto-decimal', 7 | hooks: { 8 | 'astro:config:setup': async (astro: any) => { 9 | astro.config.vite.plugins ||= [] 10 | astro.config.vite.plugins.push(unplugin.vite(options)) 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /examples/vite-vue3/src/utils/auto-decimal.ts: -------------------------------------------------------------------------------- 1 | const er = 10 2 | export function aFn(ads: number, b: number) { 3 | const result = 0.1 + 0.2 4 | console.log('result => ', result) 5 | return ads + b + '' 6 | } 7 | const sum = (0.1+0.2) + (0.1+"0.2") + (0.1+0.2) 8 | const sum1 = (0.1+0.2) +("1") 9 | console.log('sum => ', sum1) 10 | export const ads = (0.1 + 0.2) 11 | export const adc = ((.3+.2) + (.9 - .7)) / er * 100 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "DebugAutoDecimal", 8 | "runtimeArgs": [ 9 | "--loader=ts-node/esm", 10 | "--experimental-specifier-resolution=node" 11 | ], 12 | "args": ["${workspaceFolder}/src/core/transform.ts"], 13 | "sourceMaps": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /examples/vite-vue2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vite-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vite-react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/vite-vue3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vue-cli-vue2/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /examples/vite-vue2/src/components/test-jsx.jsx: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'TestJsx', 3 | data() { 4 | return { 5 | num: 0.1 + 0.2, 6 | } 7 | }, 8 | render() { 9 | return ( 10 |
11 |
12 | 自己的: 13 | {this.num} 14 |
15 | {/* next-ad-ignore */} 16 |
17 | slot: 18 | {this.$slots.default} 19 |
20 |
21 | ) 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /examples/rspack-vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-rspack-vue3", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "rsbuild dev --open", 7 | "build": "rsbuild build", 8 | "preview": "rsbuild preview" 9 | }, 10 | "dependencies": { 11 | "vue": "^3.5.13", 12 | "unplugin-auto-decimal": "workspace:*" 13 | }, 14 | "devDependencies": { 15 | "@rsbuild/core": "^1.1.8", 16 | "@rsbuild/plugin-vue": "^1.0.5", 17 | "typescript": "^5.7.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/nuxt.ts: -------------------------------------------------------------------------------- 1 | import type { AutoDecimalOptions } from './types' 2 | import { addVitePlugin, addWebpackPlugin, defineNuxtModule } from '@nuxt/kit' 3 | import unplugin from '.' 4 | import '@nuxt/schema' 5 | 6 | export interface ModuleOptions extends AutoDecimalOptions { 7 | 8 | } 9 | 10 | export default defineNuxtModule({ 11 | setup(options, _nuxt) { 12 | addVitePlugin(() => unplugin.vite(options)) 13 | addWebpackPlugin(() => unplugin.webpack(options)) 14 | 15 | // ... 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: lts/* 21 | 22 | - run: npx changelogithub 23 | env: 24 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 25 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | visit /__inspect/ to inspect the intermediate state 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vite-vue3/src/components/test.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "AutoDecimal" 7 | text: "一个将 JavaScript 中的基本运算自动转换成 decimal.js 方法的插件" 8 | actions: 9 | - theme: brand 10 | text: 什么是 AutoDecimal? 11 | link: /guide/what-is-auto-decimal 12 | - theme: alt 13 | text: 快速开始 14 | link: /guide/getting-started 15 | features: 16 | - title: 基于 Unplugin 17 | details: 支持Vite, Rollup, Webpack, Esbuild,以及基于之上构建的所有框架。 18 | - title: 自动转换 19 | details: 免除所有手动的烦恼,使你的代码更完美 20 | --- 21 | 22 | -------------------------------------------------------------------------------- /test/fixtures/test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable style/jsx-one-expression-per-line */ 2 | import React from 'react' 3 | 4 | export function TestComponent() { 5 | return ( 6 |
7 |
{0.1 + 0.2}
8 | {/* block-ad-ignore */} 9 | {0.1 + 0.2} 10 | {/* next-ad-ignore */} 11 |

{0.1 + 0.2}

12 |

13 | {/* next-ad-ignore */} 14 | skip: {0.1 + 0.2} transform: {1 - 0.9} 15 |

16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /README_NPM.md: -------------------------------------------------------------------------------- 1 |

AutoDecimal

2 |
3 | 4 | 5 | 6 |
7 | 8 | ⚡️ 支持 Vue、React 9 | 10 | ## 功能 11 | 将代码中的加、减、乘、除运算自动转为 `decimal.js` 方法,用于处理 JavaScript 中运算所造成的精度问题 12 | 13 | ## 文档 14 | [`AutoDecimal`](https://lyumg.github.io/unplugin-auto-decimal/) 15 | 16 | 17 | ## License 18 | 19 | [MIT](./LICENSE) License © 2024-PRESENT [lyumg](https://github.com/lyumg) 20 | -------------------------------------------------------------------------------- /examples/vite-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-vite-react", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "decimal.js-light": "^2.5.1", 12 | "react": "^17.0.2", 13 | "react-dom": "^17.0.2", 14 | "unplugin-auto-decimal": "workspace:*" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^17.0.33", 18 | "@types/react-dom": "^17.0.10", 19 | "@vitejs/plugin-react": "^1.0.7", 20 | "typescript": "^4.5.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/vite-vue2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-vite-vue2", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "decimal.js-light": "^2.5.1", 12 | "unplugin-auto-decimal": "workspace:*", 13 | "vite-plugin-inspect": "^0.8.1" 14 | }, 15 | "devDependencies": { 16 | "@vitejs/plugin-vue2": "^2.3.1", 17 | "@vitejs/plugin-vue2-jsx": "^1.1.1", 18 | "vite": "^5.0.9", 19 | "vue": "^2.7.14", 20 | "vue-template-compiler": "^2.7.14" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/rspack-vue3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "ES2020"], 4 | "jsx": "preserve", 5 | "target": "ES2020", 6 | "noEmit": true, 7 | "skipLibCheck": true, 8 | "jsxImportSource": "vue", 9 | "useDefineForClassFields": true, 10 | 11 | /* modules */ 12 | "module": "ESNext", 13 | "isolatedModules": true, 14 | "resolveJsonModule": true, 15 | "moduleResolution": "Bundler", 16 | "allowImportingTsExtensions": true, 17 | 18 | /* type checking */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /scripts/switch-readme.cjs: -------------------------------------------------------------------------------- 1 | // scripts/switch-readme.js 2 | const fs = require('node:fs') 3 | const path = require('node:path') 4 | const process = require('node:process') 5 | 6 | const [,,action] = process.argv 7 | 8 | const readmeNPM = path.join(__dirname, '../README_NPM.md') 9 | const readme = path.join(__dirname, '../README.md') 10 | 11 | if (action === 'prepare') { 12 | if (fs.existsSync(readme)) { 13 | fs.copyFileSync(readme, `${readme}.backup`) 14 | } 15 | fs.copyFileSync(readmeNPM, readme) 16 | } 17 | else if (action === 'restore') { 18 | if (fs.existsSync(`${readme}.backup`)) { 19 | fs.renameSync(`${readme}.backup`, readme) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/vite-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

AutoDecimal

6 |
7 | 8 | 9 | 10 |
11 | 12 | ⚡️ 支持 Vue、React 13 | 14 | ## 功能 15 | 将代码中的加、减、乘、除运算自动转为 `decimal.js` 方法,用于处理 JavaScript 中运算所造成的精度问题 16 | 17 | ## 文档 18 | [`AutoDecimal`](https://lyumg.github.io/unplugin-auto-decimal/) 19 | 20 | 21 | ## License 22 | 23 | [MIT](./LICENSE) License © 2024-PRESENT [lyumg](https://github.com/lyumg) 24 | -------------------------------------------------------------------------------- /examples/vue-cli-vue3/src/components/test-jsx.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vue-cli-vue2/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/vue-cli-vue3/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/vite-vue3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "jsx": "preserve", 5 | "lib": ["esnext", "dom"], 6 | "useDefineForClassFields": true, 7 | "baseUrl": "./", 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "paths": { 11 | "@": ["src"], 12 | "@/*": ["src/*"] 13 | }, 14 | "resolveJsonModule": true, 15 | "strict": true, 16 | "sourceMap": true, 17 | "esModuleInterop": true, 18 | "types": ["./auto-decimal.d.ts", "./global.d.ts"] 19 | }, 20 | "references": [{ "path": "./tsconfig.node.json" }], 21 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "../../src/index.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /examples/vue-cli-vue2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-vue-cli-vue2", 3 | "private": true, 4 | "scripts": { 5 | "build": "vue-cli-service build", 6 | "dev": "vue-cli-service serve", 7 | "lint": "vue-cli-service lint" 8 | }, 9 | "dependencies": { 10 | "@vue/composition-api": "^1.7.2", 11 | "core-js": "^3.39.0", 12 | "vue": "^2.7.16" 13 | }, 14 | "devDependencies": { 15 | "@vue/cli-plugin-babel": "^5.0.8", 16 | "@vue/cli-plugin-typescript": "^5.0.8", 17 | "@vue/cli-service": "^5.0.8", 18 | "typescript": "^5.7.2", 19 | "unplugin-icons": "^0.20.2", 20 | "unplugin-auto-decimal": "workspace:*", 21 | "unplugin-vue2-script-setup": "^0.11.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/vue-cli-vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-vue-cli-vue3", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.39.0", 12 | "vue": "^3.5.13" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.26.0", 16 | "@vue/babel-plugin-jsx": "^1.2.5", 17 | "@vue/cli-plugin-babel": "~5.0.8", 18 | "@vue/cli-service": "~5.0.8", 19 | "unplugin-auto-decimal": "workspace:*" 20 | }, 21 | "browserslist": [ 22 | "> 1%", 23 | "last 2 versions", 24 | "not dead", 25 | "not ie 11" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/vite-vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-vite-vue3", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@vitejs/plugin-vue-jsx": "^3.1.0", 13 | "@vueuse/core": "^8.9.4", 14 | "decimal.js-light": "^2.5.1", 15 | "unplugin-auto-decimal": "workspace:*", 16 | "vue": "^3.3.11", 17 | "vue-router": "^4.2.5" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^17.0.45", 21 | "@vitejs/plugin-vue": "^2.3.4", 22 | "typescript": "^4.9.5", 23 | "vite-plugin-inspect": "^0.8.1", 24 | "vue-tsc": "^1.8.25" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/vite-vue3/vite.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import vueJsx from '@vitejs/plugin-vue-jsx' 5 | import Inspect from 'vite-plugin-inspect' 6 | import AutoDecimal from 'unplugin-auto-decimal/vite' 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | 10 | resolve: { 11 | alias: { 12 | '@': path.resolve(__dirname, 'src'), 13 | }, 14 | }, 15 | build: { 16 | minify: false, 17 | }, 18 | plugins: [AutoDecimal({ toDecimal: true, dts: false }), vue(), vueJsx(), Inspect()], 19 | server: { 20 | port: 8080, 21 | hmr: { 22 | host: 'localhost', 23 | port: 8080, 24 | }, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /examples/vue-cli-vue2/vue.config.js: -------------------------------------------------------------------------------- 1 | const AutoDecimal = require('unplugin-auto-decimal/webpack') 2 | 3 | /** 4 | * @type {import('@vue/cli-service').ProjectOptions} 5 | */ 6 | module.exports = { 7 | configureWebpack: { 8 | plugins: [ 9 | AutoDecimal(), 10 | ], 11 | }, 12 | chainWebpack(config) { 13 | // disable type check and let `vue-tsc` handles it 14 | config.plugins.delete('fork-ts-checker') 15 | 16 | // disable cache for testing, you should remove this in production 17 | config.module.rule('vue').uses.delete('cache-loader') 18 | config.module.rule('js').uses.delete('cache-loader') 19 | config.module.rule('ts').uses.delete('cache-loader') 20 | config.module.rule('tsx').uses.delete('cache-loader') 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/to-decimal.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable ts/ban-ts-comment */ 2 | // @ts-nocheck 3 | const _a = 0.1 + 0.2.toDecimal() 4 | const _s = 0.1 + 0.2.toDecimal() 5 | 6 | function _test() { 7 | const _block = 0.1 + 0.2 8 | const _ad = 0.111 + 0.222.toDecimal({ precision: 3, callMethod: 'toFixed' }) 9 | } 10 | class _BlockAd { 11 | private block: number 12 | constructor() { 13 | this.block = 0.1 + 0.2.toDecimal() 14 | } 15 | 16 | calc() { 17 | this.block = this.block + 0.7 - 0.9 18 | } 19 | } 20 | const _splicing = 0.1 + 0.2 21 | const _arr = [0, 0.1 + 0.2.toDecimal({ callMethod: 'toString' }), 3] 22 | const _obj_outer = { 23 | transform: 0.1 + 0.2, 24 | skip: 0.1 + 0.2, 25 | } 26 | 27 | const _toDecimal = (0.111 + 0.222).toDecimal({ callMethod: 'decimal' }) 28 | _toDecimal.toNumber() 29 | -------------------------------------------------------------------------------- /examples/vite-react/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | button { 41 | font-size: calc(10px + 2vmin); 42 | } 43 | -------------------------------------------------------------------------------- /docs/guide/api/index.md: -------------------------------------------------------------------------------- 1 | # 配置选项 2 | 3 | `AutoDecimal` 为了提供良好的开发体验,提供了几个 API 来让你尽量使用的没有心智负担。但是世上总是没有尽善尽美的事情,所以有一些配置,你是需要知道的。 4 | 5 | 如果你是想要在一个即将开始的项目中使用的话,下面的几个配置项是可以跳过的。 6 | 7 | | 属性 | 描述 | 类型 | 默认值 | 8 | | ---------------- | :------: | :------: |:------: | 9 | | [`tailPatchZero`](./tail-patch-zero.md) | 区分计算表达式和字符串拼接 | boolean | false | 10 | | [`supportString`](./support-string.md) | 支持字符串计算 | boolean | false | 11 | | package | 高精度计算库 | `decimal.js`、`decimal.js-light`、`big.js` | `decimal.js-light` | 12 | | [`toDecimal`](./to-decimal.md) ^(1.2.0) | 使用 `toDecimal` 进行转换 | boolean \| options | false | 13 | | dts ^(1.2.0) | 生成.d.ts 文件。 如果本地安装了 `typescript` 默认为 true,js 项目需要手动设置为 false | boolean \| string | false | 14 | | [`supportNewFunction`](./new-function.md) ^(1.4.0) | 支持处理 new Function | boolean \| options | false | -------------------------------------------------------------------------------- /docs/guide/comment/splicing.md: -------------------------------------------------------------------------------- 1 | # 末尾拼接字符串 2 | 3 | 在一个计算表达式的末尾拼接上一个空字符串。 4 | :::code-group 5 | ```ts [vite.config.ts] {3} 6 | export default defineConfig({ 7 | plugins: [ 8 | AutoDecimal({ supportString: true }) 9 | ] 10 | }) 11 | ``` 12 | ::: 13 | ```ts {2-3,5-6} 14 | const a = 0.2 15 | const b = a + 0.1 16 | console.log(b, '0.3') 17 | 18 | const c = a + 0.1 + '' 19 | console.log(c, '0.30000000000000004') 20 | 21 | ``` 22 | 23 | 当`supportString: false`, 可以在一个计算表达式的末尾拼接任意字符串。 24 | :::code-group 25 | ```ts [vite.config.ts] {3} 26 | export default defineConfig({ 27 | plugins: [ 28 | AutoDecimal({ supportString: false }) 29 | ] 30 | }) 31 | ``` 32 | ::: 33 | ```ts {2-3,5-6} 34 | const a = 0.2 35 | const b = a + 0.1 36 | console.log(b, '0.3') 37 | 38 | const c = a + 0.1 + '1' 39 | console.log(c, '0.300000000000000041') 40 | 41 | ``` -------------------------------------------------------------------------------- /examples/vue-cli-vue3/src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /test/fixtures/new-function-to-decimal.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable no-new-func */ 3 | let fnString = '' 4 | const returnedValue = () => 'return a + b + 3' 5 | function returnedValue2() { 6 | return 'return a + b + 4..toDecimal()' 7 | } 8 | fnString = returnedValue() 9 | function runFn(num: any, params: any) { 10 | console.log('inner, toDecimal: true, ', num, ' + 0.2;', fnString) 11 | const arr = [1, new Function('a', 'b', params)] 12 | const obj = { b: new Function('a', 'b', params) } 13 | const fn = new Function('a', 'b', params) 14 | console.log('obj.b', obj.b(num, 0.2)) 15 | // @ts-expect-error array new Function 16 | console.log('arr[1]', arr[1](0.1, 0.2)) 17 | const callFn = fn(num, 0.2) 18 | console.log(callFn, 'callFn') 19 | } 20 | runFn('12', fnString) 21 | fnString = returnedValue2() 22 | runFn('12', fnString) 23 | -------------------------------------------------------------------------------- /examples/vite-vue3/src/types/shims-axios.d.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios'; 2 | /** 3 | * 自定义扩展axios模块 4 | * @author Maybe 5 | */ 6 | declare module 'axios' { 7 | export interface AxiosInstance { 8 | (config: AxiosRequestConfig): Promise; 9 | request(config: AxiosRequestConfig): Promise; 10 | get(url: string, config?: AxiosRequestConfig): Promise; 11 | delete(url: string, config?: AxiosRequestConfig): Promise; 12 | head(url: string, config?: AxiosRequestConfig): Promise; 13 | post(url: string, data?: any, config?: AxiosRequestConfig): Promise; 14 | put(url: string, data?: any, config?: AxiosRequestConfig): Promise; 15 | patch(url: string, data?: any, config?: AxiosRequestConfig): Promise; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/guide/what-is-auto-decimal.md: -------------------------------------------------------------------------------- 1 | # AutoDecimal 是什么? 2 | 3 | `AutoDecimal` 是一个基于 [`unplugin`](https://unplugin.unjs.io/) 构建的自动转换插件,能够自动将 JavaScript 中的加、减、乘、除转换为 [`decimal.js`](https://mikemcl.github.io/decimal.js/) 中的方法,从而避免手动转换所带来的种种不便。 4 | 5 | ## 使用场景 6 | 7 | - 当你的项目中需要高精度的计算 8 | - 当你使用高精度计算库,厌倦了每个计算都要手动引用并转换成该库的方法 9 | - 当你想要美观且直白的运算,同时又不想被计算所造成的精度所困扰 10 | 11 | ## 开发体验 12 | 13 | `AutoDecimal` 让你在编写代码时可以像往常一样使用基本运算符。它会在构建时自动处理转换,无需手动修改每一行代码。这种自动化的过程大大减少了开发时间和精力。 14 | 15 | - 基于拥有广大用户稳定且开源的高精度库 `decimal.js` 16 | - 基于为各种构建工具提供统一插件的 `unplugin` 17 | 18 | **未使用 `AutoDecimal` 时** 19 | 20 | ```js{4} 21 | const num = 0.1 22 | const otherNum = 0.2 23 | const sum = num + otherNum 24 | console.log(sum, '输出0.30000000000000004') 25 | ``` 26 | 27 | **使用 `AutoDecimal` 后** 28 | 29 | ```js{4} 30 | const num = 0.1 31 | const otherNum = 0.2 32 | const sum = num + otherNum 33 | console.log(sum, '输出0.3') 34 | ``` 35 | 36 | ### 两个字 `完美` -------------------------------------------------------------------------------- /test/fixtures/test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable ts/ban-ts-comment */ 2 | // @ts-nocheck 3 | const _a = 0.1 + 0.2 4 | // next-ad-ignore 5 | const _s = 0.1 + 0.2 6 | 7 | const _computation = (0.1 + 0.2) * (1 - 0.9) + 0.5 * 0.6 / (1 - 0.2) + 0.5 8 | 9 | // block-ad-ignore 10 | function _test() { 11 | const _block = 0.1 + 0.2 12 | const _ad = 0.1 + 0.2 13 | } 14 | // block-ad-ignore 15 | { 16 | const _obj = 0.1 + 0.2 17 | const _obj_block = 0.1 + 0.2 18 | } 19 | 20 | class _BlockAd { 21 | private block: number 22 | constructor() { 23 | this.block = 0.1 + 0.2 24 | } 25 | 26 | calc() { 27 | this.block = this.block + 0.7 - 0.9 28 | } 29 | } 30 | // eslint-disable-next-line prefer-template 31 | const _splicing = 0.1 + 0.2 + '' 32 | const _arr = [0, 0.1 + 0.2, 3] 33 | const _obj_outer = { 34 | transform: 0.1 + 0.2, 35 | // next-ad-ignore 36 | skip: 0.1 + 0.2, 37 | } 38 | 39 | const integer = 1 + 2 + 3 40 | 41 | const _mix = integer * (3 + 4) - (5 - 6 + 0.4) 42 | -------------------------------------------------------------------------------- /docs/guide/api/support-string.md: -------------------------------------------------------------------------------- 1 | # 支持字符串计算 2 | 3 | `supportString` 支持字符串数字进行运算,也就是说当你启用了这个属性的话,那么在你的项目中就不会出现数字字符串拼接的情况了 4 | :::tip 5 | 这里的支持也仅仅只是支持可以转成数字的字符转,不可转换的字符串会跳过 6 | ::: 7 | :::code-group 8 | ```ts [vite.config.ts] 9 | export default defineConfig({ 10 | plugins: [ 11 | AutoDecimal({ 12 | supportString: true 13 | }) 14 | ] 15 | }) 16 | ``` 17 | ::: 18 | 19 | ```ts { 5,11-12,15-16 } 20 | const a = '1' 21 | const b = a + 1 22 | console.log(b, '这里的结果是 2,而不是 “11”') 23 | 24 | const c = '1' 25 | const d = c + '我也试试' 26 | console.log(d, '1我也试试') 27 | ``` 28 | 29 | :::warning 30 | 如果一个计算表达式中存在变量时,`AutoDecimal` 不会检索变量的值是否合法。 31 | ::: 32 | 将上面的示例稍微改写一下 33 | 34 | ```ts { 7-11 } 35 | const a = '1' 36 | const b = a + 1 37 | // 这样是可以的 38 | console.log(b, '这里的结果是 2,而不是 “11”') 39 | 40 | // 改写 41 | // const c = '1' 42 | // const d = c + '我也试试' 43 | const c = '我也试试' 44 | // 此处仍然会转换为 const d = new __Decimal(c).plus(1).toNumber() 45 | // 所以这里 Decimal 会报错,因为 c 不是一个有效的数值 46 | const d = c + 1 47 | console.log(d, '1我也试试') 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/.vitepress/plugins/tag.ts: -------------------------------------------------------------------------------- 1 | import type { MarkdownRenderer } from 'vitepress' 2 | 3 | export default (md: MarkdownRenderer): void => { 4 | md.inline.ruler.before('emphasis', 'tag', (state, silent) => { 5 | const tagRegExp = /^\^\(([^)]*)\)/ 6 | const str = state.src.slice(state.pos, state.posMax) 7 | 8 | if (!tagRegExp.test(str)) 9 | return false 10 | if (silent) 11 | return true 12 | 13 | const result = str.match(tagRegExp) 14 | 15 | if (!result) 16 | return false 17 | 18 | const token = state.push('html_inline', '', 0) 19 | const value = result[1].trim() 20 | /** 21 | * Add styles for some special tags 22 | * vitepress/styles/content/tag-content.scss 23 | */ 24 | const tagClass = ['beta', 'deprecated', 'a11y', 'required'].includes(value) 25 | ? value 26 | : '' 27 | token.content = `${value}` 28 | token.level = state.level 29 | state.pos += result[0].length 30 | 31 | return true 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #833edf; 3 | --warning: #eba34a; 4 | --danger: #fd6d6f; 5 | --success: #58c348; 6 | --vp-layout-max-width: 1600px; 7 | } 8 | .VPDoc.has-aside .content-container { 9 | max-width: 100% !important; 10 | } 11 | .vp-doc td{ 12 | padding: .6em 1em; 13 | text-align: left; 14 | white-space: pre-wrap; 15 | } 16 | .vp-tag { 17 | --vp-tag-color: var(--primary); 18 | --vp-tag-border-color: var(--primary); 19 | 20 | display: inline-block; 21 | padding: 0 7px; 22 | border-radius: 10px; 23 | border: 1px solid var(--vp-tag-border-color); 24 | font-size: 12px; 25 | color: var(--vp-tag-color); 26 | line-height: 18px; 27 | white-space: nowrap; 28 | 29 | &.beta { 30 | --vp-tag-color: var(--danger); 31 | --vp-tag-border-color: var(--danger); 32 | } 33 | &.deprecated { 34 | --vp-tag-color: var(--warning); 35 | --vp-tag-border-color: var(--warning); 36 | } 37 | &.required { 38 | --vp-tag-color: var(--success); 39 | --vp-tag-border-color: var(--success); 40 | } 41 | } -------------------------------------------------------------------------------- /test/fixtures/new-function.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable unused-imports/no-unused-vars */ 3 | /* eslint-disable no-new-func */ 4 | const fn = new Function('a', 'b', `return a + b`) 5 | const result = fn(0.1, 0.2) 6 | let fnTxt = '' 7 | fnTxt = '1' 8 | fnTxt = 'return a + b' 9 | if (true) { 10 | fnTxt = 'return a + b + 1' 11 | } 12 | else { 13 | fnTxt = 'return a + b + 0' 14 | } 15 | const returnValue = () => 'return a + b + 3' 16 | function returnValue2() { 17 | return 'return a + b + 4..toDecimal()' 18 | } 19 | fnTxt = returnValue() 20 | function run(num: any, params: any) { 21 | console.log('inner, toDecimal: true, ', num, ' + 0.2;', fnTxt) 22 | const arr = [1, new Function('a', 'b', params)] 23 | const obj = { b: new Function('a', 'b', params) } 24 | const fn = new Function('a', 'b', params) 25 | console.log('obj.b', obj.b(num, 0.2)) 26 | // @ts-expect-error array new Function 27 | console.log('arr[1]', arr[1](0.1, 0.2)) 28 | const callFn = fn(num, 0.2) 29 | console.log(callFn, 'callFn') 30 | } 31 | run('12', fnTxt) 32 | fnTxt = returnValue2() 33 | run('12', fnTxt) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Lyu 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. -------------------------------------------------------------------------------- /test/fixtures/new-function-inject-window.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line ts/ban-ts-comment 2 | // @ts-nocheck 3 | /* eslint-disable no-console */ 4 | /* eslint-disable unused-imports/no-unused-vars */ 5 | /* eslint-disable no-new-func */ 6 | const fn = new Function('a', 'b', `return a + b`) 7 | const result = fn(0.1, 0.2) 8 | let fnTxt = '' 9 | fnTxt = '1' 10 | fnTxt = 'return a + b' 11 | if (true) { 12 | fnTxt = 'return a + b + 1' 13 | } 14 | else { 15 | fnTxt = 'return a + b + 0' 16 | } 17 | const returnValue = () => 'return a + b + 3' 18 | function returnValue2() { 19 | return 'return a + b + 4..toDecimal()' 20 | } 21 | fnTxt = returnValue() 22 | function run(num: any, params: any) { 23 | console.log('inner, toDecimal: true, ', num, ' + 0.2;', fnTxt) 24 | const arr = [1, new Function('a', 'b', params)] 25 | const obj = { b: new Function('a', 'b', params) } 26 | const fn = new Function('a', 'b', params) 27 | console.log('obj.b', obj.b(num, 0.2)) 28 | // @ts-expect-error array new Function 29 | console.log('arr[1]', arr[1](0.1, 0.2)) 30 | const callFn = fn(num, 0.2) 31 | console.log(callFn, 'callFn') 32 | } 33 | run('12', fnTxt) 34 | fnTxt = returnValue2() 35 | run('12', fnTxt) 36 | -------------------------------------------------------------------------------- /examples/vite-vue2/App.vue: -------------------------------------------------------------------------------- 1 | 51 | -------------------------------------------------------------------------------- /test/fixtures/options.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | permissions: 15 | id-token: write 16 | actions: write 17 | pull-requests: write 18 | statuses: write 19 | contents: write 20 | issues: write 21 | security-events: write 22 | pages: read 23 | 24 | steps: 25 | - name: Checkout Repo 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | 30 | - uses: pnpm/action-setup@v4.0.0 31 | 32 | - name: Setup Node.js 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: lts/* 36 | registry-url: 'https://registry.npmjs.org/' 37 | cache: pnpm 38 | 39 | - name: Install dependencies 40 | run: pnpm install 41 | 42 | - name: Build 43 | run: pnpm build 44 | 45 | - name: Npm Publish 46 | run: npm publish 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 49 | NPM_CONFIG_PROVENANCE: true 50 | -------------------------------------------------------------------------------- /src/core/generate.ts: -------------------------------------------------------------------------------- 1 | import type { InnerAutoDecimalOptions } from '../types' 2 | import { existsSync } from 'node:fs' 3 | import { mkdir, readFile, writeFile } from 'node:fs/promises' 4 | import { dirname } from 'node:path' 5 | import { DEFAULT_TO_DECIMAL_CONFIG } from './constant' 6 | 7 | export async function generateDeclaration(options: InnerAutoDecimalOptions) { 8 | const filePath = options.dts as string 9 | const toDecimal = typeof options.toDecimal === 'boolean' ? DEFAULT_TO_DECIMAL_CONFIG : options.toDecimal 10 | const content = `/* eslint-disable */ 11 | // @ts-nocheck 12 | // Generated by unplugin-auto-decimal 13 | type ToDecimal = import('unplugin-auto-decimal/types').ToDecimal 14 | declare interface String { 15 | ${toDecimal.name}: ToDecimal 16 | } 17 | declare interface Number { 18 | ${toDecimal.name}: ToDecimal 19 | } 20 | ` 21 | const originalContent = existsSync(filePath) ? await readFile(filePath, 'utf-8') : '' 22 | if (originalContent !== content) { 23 | await writeDeclaration(filePath, content) 24 | } 25 | } 26 | async function writeDeclaration(filePath: string, content: string) { 27 | await mkdir(dirname(filePath), { recursive: true }) 28 | return await writeFile(filePath, content, 'utf-8') 29 | } 30 | -------------------------------------------------------------------------------- /docs/guide/api/tail-patch-zero.md: -------------------------------------------------------------------------------- 1 | # 末位补 0 2 | 3 | 在以往很多的项目中,可能或多或少的使用过 “+” 来进行字符串的拼接组合,甚至很多会在末尾添加一个空的字符串用来将某个变量变成字符串。 4 | 5 | 其实这么做也是无可厚非的,毕竟在 JavaScript 中是允许的。但是这样的结果就是,如果使用`AutoDecimal`的话,可能你的项目会无法运行,因为`decimal.js`是计算数字的,而不能计算无法被解析为数字的字符串。 6 | 7 | 另外因为 JavaScript 的灵活性,也造成了无法解析 “+” 是加法还是字符串拼接,所以要用 `tailPatchZero` 这个配置项来解决这个问题。 8 | ::: tip 9 | 在新的项目中,提倡使用字符串模板来进行拼接组合字符串,这样就可以拿掉这个心智负担了。可以尽情的写计算而不用担心精度问题。 10 | ::: 11 | 12 | `tailPatchZero` 就是在计算表达式的末位手动添加一个 `+ 0 `来告诉 `AutoDecimal`,这是一个计算表达式,你可以尽情的转换不用担心 `decimal.js` 会报错。 13 | :::code-group 14 | ```ts [vite.config.ts] 15 | export default defineConfig({ 16 | plugins: [ 17 | AutoDecimal({ 18 | tailPatchZero: true 19 | }) 20 | ] 21 | }) 22 | ``` 23 | ::: 24 | 25 | ```ts { 5,14-15,18-19,22-23 } 26 | // ...someone.ts 27 | const a = 0.1 28 | const b = 0.2 29 | // 已经启用了末位补 0 ,这样的计算表达式不会进行转换 30 | const c = a + b 31 | console.log(c, '0.30000000000000004') 32 | 33 | // 通过末位补 0 , 告诉 AutoDecimal 它是可以转换的 34 | const d = a + b + 0 35 | console.log(d, '0.3') 36 | 37 | // 当然这种是不需要 + 0 的 38 | const e = 0.1 + 0.2 39 | console.log(e, '0.3') 40 | 41 | // 这种会直接转换, 因为只有末位为加法才会有限制 42 | const f = 0.1 + 0.3 - 0.1 43 | console.log(f, '0.3') 44 | ``` 45 | :::tip 46 | `tailPatchZero` 只会影响加法的转换,其他的运算可以忽略它,另外只有在末位是加法的表达式才会跳过转换 47 | ::: -------------------------------------------------------------------------------- /examples/rspack-vue3/src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | 48 | -------------------------------------------------------------------------------- /test/fixtures/setup.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 52 | -------------------------------------------------------------------------------- /src/core/traverse/import-declaration.ts: -------------------------------------------------------------------------------- 1 | import type { NodePath } from '@babel/traverse' 2 | import type { ImportDeclaration } from '@babel/types' 3 | import type { Options } from '../../types' 4 | import { isIdentifier, isImportDefaultSpecifier, isImportNamespaceSpecifier } from '@babel/types' 5 | import { DECIMAL_PKG_NAME, PKG_NAME } from '../constant' 6 | 7 | export function resolveImportDeclaration(path: NodePath, options: Options) { 8 | if (path.node.source.value === PKG_NAME) { 9 | const defaultDecimalPkgName = options.autoDecimalOptions.decimalName || DECIMAL_PKG_NAME 10 | options.imported = path.node.specifiers.some((spec) => { 11 | if (isImportDefaultSpecifier(spec)) { 12 | if (spec.local.name !== defaultDecimalPkgName) { 13 | options.decimalPkgName = spec.local.name 14 | } 15 | return true 16 | } 17 | if (isImportNamespaceSpecifier(spec)) { 18 | const pkgName = options.autoDecimalOptions.package === 'big.js' ? 'Big' : 'Decimal' 19 | options.decimalPkgName = `${spec.local.name}.${pkgName}` 20 | return true 21 | } 22 | if (isIdentifier(spec.imported) && spec.imported.name !== defaultDecimalPkgName) { 23 | options.decimalPkgName = spec.local.name 24 | return true 25 | } 26 | return false 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy VitePress site to Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: pages 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | # 构建工作 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - uses: pnpm/action-setup@v3 28 | with: 29 | version: 9 30 | - name: Setup Node 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: 20 34 | cache: pnpm 35 | - name: Setup Pages 36 | uses: actions/configure-pages@v4 37 | - name: Install dependencies 38 | run: pnpm install 39 | - name: Build with VitePress 40 | run: pnpm docs:build 41 | - name: Upload artifact 42 | uses: actions/upload-pages-artifact@v3 43 | with: 44 | path: docs/.vitepress/dist 45 | 46 | # 部署工作 47 | deploy: 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | needs: build 52 | runs-on: ubuntu-latest 53 | name: Deploy 54 | steps: 55 | - name: Deploy to GitHub Pages 56 | id: deployment 57 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /examples/vite-react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import logo from './logo.svg' 3 | import './App.css' 4 | 5 | function App() { 6 | const [count, setCount] = useState(0) 7 | 8 | return ( 9 |
10 |
11 | logo 12 | {/* block-ad-ignore */} 13 |

14 | Hello Vite + React! autoDecimal => 15 | {0.1 + 0.2} 16 |

17 |

18 | 23 |

24 |

25 | Edit 26 | {' '} 27 | App.tsx 28 | {' '} 29 | and save to test HMR updates. 30 |

31 |

32 | 38 | Learn React 39 | 40 | {' | '} 41 | 47 | Vite Docs 48 | 49 |

50 |
51 |
52 | ) 53 | } 54 | 55 | export default App 56 | -------------------------------------------------------------------------------- /examples/vite-vue2/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/vite-react/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # Nuxt generate 71 | dist 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless 78 | 79 | # IDE 80 | .idea 81 | 82 | .DS_Store 83 | docs/.vitepress/cache 84 | README.md.* -------------------------------------------------------------------------------- /examples/vite-vue3/src/App.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 51 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Disable the default formatter, use eslint instead 3 | "prettier.enable": false, 4 | "editor.formatOnSave": false, 5 | 6 | // Auto fix 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit", 9 | "source.organizeImports": "never" 10 | }, 11 | 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { "rule": "style/*", "severity": "off", "fixable": true }, 15 | { "rule": "format/*", "severity": "off", "fixable": true }, 16 | { "rule": "*-indent", "severity": "off", "fixable": true }, 17 | { "rule": "*-spacing", "severity": "off", "fixable": true }, 18 | { "rule": "*-spaces", "severity": "off", "fixable": true }, 19 | { "rule": "*-order", "severity": "off", "fixable": true }, 20 | { "rule": "*-dangle", "severity": "off", "fixable": true }, 21 | { "rule": "*-newline", "severity": "off", "fixable": true }, 22 | { "rule": "*quotes", "severity": "off", "fixable": true }, 23 | { "rule": "*semi", "severity": "off", "fixable": true } 24 | ], 25 | 26 | // Enable eslint for all supported languages 27 | "eslint.validate": [ 28 | "javascript", 29 | "javascriptreact", 30 | "typescript", 31 | "typescriptreact", 32 | "vue", 33 | "html", 34 | "markdown", 35 | "json", 36 | "jsonc", 37 | "yaml", 38 | "toml", 39 | "xml", 40 | "gql", 41 | "graphql", 42 | "astro", 43 | "svelte", 44 | "css", 45 | "less", 46 | "scss", 47 | "pcss", 48 | "postcss" 49 | ], 50 | "cSpell.words": [ 51 | "bumpp", 52 | "esno", 53 | "farmfe", 54 | "nuxt", 55 | "pluginutils", 56 | "postbuild", 57 | "rspack" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src/core/unplugin.ts: -------------------------------------------------------------------------------- 1 | import type { MagicStringAST } from 'magic-string-ast' 2 | import type { AutoDecimalOptions, InnerAutoDecimalOptions } from '../types' 3 | import { createFilter } from '@rollup/pluginutils' 4 | import { isPackageExists } from 'local-pkg' 5 | import { createUnplugin } from 'unplugin' 6 | import { PKG_NAME, REGEX_NODE_MODULES, REGEX_SUPPORTED_EXT, REGEX_VUE } from './constant' 7 | import { generateDeclaration } from './generate' 8 | import { resolveOptions } from './options' 9 | import { transformAutoDecimal, transformVueAutoDecimal } from './transform' 10 | 11 | export function transform(code: string, id: string, options: InnerAutoDecimalOptions) { 12 | let msa: MagicStringAST 13 | if (REGEX_VUE.some(reg => reg.test(id))) { 14 | msa = transformVueAutoDecimal(code, options) 15 | } 16 | else { 17 | msa = transformAutoDecimal(code, options) 18 | } 19 | if (!msa.hasChanged()) 20 | return 21 | return { 22 | code: msa.toString(), 23 | map: msa.generateMap({ source: id, includeContent: true, hires: true }), 24 | } 25 | } 26 | export default createUnplugin((rawOptions) => { 27 | const filter = createFilter( 28 | [REGEX_SUPPORTED_EXT, ...REGEX_VUE], 29 | [REGEX_NODE_MODULES], 30 | ) 31 | const options = resolveOptions(rawOptions) 32 | if (options.dts) { 33 | generateDeclaration(options) 34 | } 35 | return { 36 | name: 'unplugin-auto-decimal', 37 | enforce: 'pre', 38 | transformInclude(id) { 39 | return filter(id) 40 | }, 41 | transform(code, id) { 42 | const pkgName = options.package ?? PKG_NAME 43 | if (!isPackageExists(pkgName)) { 44 | console.error(`[AutoDecimal] 请先安装 ${pkgName}`) 45 | return { code } 46 | } 47 | return transform(code, id, options) 48 | }, 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /src/core/constant.ts: -------------------------------------------------------------------------------- 1 | import type { InnerToDecimalOptions } from '../types' 2 | 3 | export const PREFIX = 'ad' 4 | export const BASE_COMMENT = `${PREFIX}-ignore` 5 | export const NEXT_COMMENT = `next-${BASE_COMMENT}` 6 | export const FILE_COMMENT = `file-${BASE_COMMENT}` 7 | export const BLOCK_COMMENT = `block-${BASE_COMMENT}` 8 | export const COMMENTS = [BASE_COMMENT, NEXT_COMMENT] 9 | export const PATCH_DECLARATION = 'const __PATCH_DECLARATION__ = ' 10 | export const RETURN_DECLARATION_CODE = '###code###' 11 | export const RETURN_FUNCTION_NAME = '__RETURN_DECLARATION_FN__' 12 | export const RETURN_DECLARATION_PREFIX = `function ${RETURN_FUNCTION_NAME}() {` 13 | export const RETURN_DECLARATION_FN = `${RETURN_DECLARATION_PREFIX}${RETURN_DECLARATION_CODE}}` 14 | export const LITERALS = ['StringLiteral', 'NullLiteral', 'BooleanLiteral', 'TemplateLiteral'] 15 | export const OPERATOR = { 16 | '+': 'plus', 17 | '-': 'minus', 18 | '*': 'times', 19 | '/': 'div', 20 | '**': 'pow', 21 | } 22 | 23 | export const OPERATOR_KEYS = Object.keys(OPERATOR) 24 | export const REGEX_SUPPORTED_EXT = /\.([cm]?[jt]s)x?$/ 25 | export const REGEX_VUE = [/\.vue$/, /\.vue\?vue/, /\.vue\?v=/] 26 | export const REGEX_NODE_MODULES = /node_modules/ 27 | export const DECIMAL_PKG_NAME = '__Decimal' 28 | export const PKG_NAME = 'decimal.js-light' 29 | export const DEFAULT_TO_DECIMAL_CONFIG: InnerToDecimalOptions = { 30 | precision: 2, 31 | p: 2, 32 | roundingModes: 'ROUND_HALF_UP', 33 | rm: 'ROUND_HALF_UP', 34 | callMethod: 'toNumber', 35 | cm: 'toNumber', 36 | name: 'toDecimal', 37 | } 38 | export const DEFAULT_NEW_FUNCTION_CONFIG = { 39 | injectWindow: undefined, 40 | toDecimal: false, 41 | } as const 42 | export const DECIMAL_RM_LIGHT = Object.freeze({ 43 | ROUND_UP: 0, 44 | ROUND_DOWN: 1, 45 | ROUND_CEIL: 2, 46 | ROUND_FLOOR: 3, 47 | ROUND_HALF_UP: 4, 48 | ROUND_HALF_DOWN: 5, 49 | ROUND_HALF_EVEN: 6, 50 | ROUND_HALF_CEIL: 7, 51 | ROUND_HALF_FLOOR: 8, 52 | }) 53 | export const DECIMAL_RM = Object.freeze({ 54 | ...DECIMAL_RM_LIGHT, 55 | EUCLID: 9, 56 | }) 57 | export const BIG_RM = Object.freeze({ 58 | ROUND_DOWN: 0, 59 | ROUND_HALF_UP: 1, 60 | ROUND_HALF_DOWN: 2, 61 | ROUND_UP: 3, 62 | }) 63 | -------------------------------------------------------------------------------- /src/core/options.ts: -------------------------------------------------------------------------------- 1 | import type { AutoDecimalOptions, InnerAutoDecimalOptions, InnerToDecimalOptions, ToDecimalOptions } from '../types' 2 | import { resolve } from 'node:path' 3 | import process from 'node:process' 4 | import { isPackageExists } from 'local-pkg' 5 | import { DEFAULT_NEW_FUNCTION_CONFIG, DEFAULT_TO_DECIMAL_CONFIG } from './constant' 6 | 7 | const rootPath = process.cwd() 8 | const defaultOptions: InnerAutoDecimalOptions = { 9 | supportString: false, 10 | tailPatchZero: false, 11 | package: 'decimal.js-light', 12 | toDecimal: false, 13 | dts: isPackageExists('typescript'), 14 | supportNewFunction: false, 15 | decimalName: '__Decimal', 16 | } 17 | export function resolveOptions(rawOptions?: AutoDecimalOptions): InnerAutoDecimalOptions { 18 | const options = Object.assign({}, defaultOptions, rawOptions) 19 | options.dts = !options.dts 20 | ? false 21 | : resolve(rootPath, typeof options.dts === 'string' ? options.dts : 'auto-decimal.d.ts') 22 | options.toDecimal = !options.toDecimal 23 | ? false 24 | : options.toDecimal === true 25 | ? { ...DEFAULT_TO_DECIMAL_CONFIG } 26 | : Object.assign({}, DEFAULT_TO_DECIMAL_CONFIG, options.toDecimal) 27 | options.supportNewFunction = !options.supportNewFunction 28 | ? false 29 | : options.supportNewFunction === true 30 | ? { toDecimal: options.toDecimal } 31 | : { 32 | ...DEFAULT_NEW_FUNCTION_CONFIG, 33 | toDecimal: options.toDecimal, 34 | ...options.supportNewFunction, 35 | } 36 | return options 37 | } 38 | export function mergeToDecimalOptions(rawOptions: InnerToDecimalOptions, toDecimalOptions: ToDecimalOptions | boolean) { 39 | if (typeof toDecimalOptions === 'boolean') { 40 | return rawOptions 41 | } 42 | const precision = toDecimalOptions.precision ?? toDecimalOptions.p ?? rawOptions.precision 43 | const callMethod = toDecimalOptions.callMethod ?? toDecimalOptions.cm ?? rawOptions.callMethod 44 | const roundingModes = toDecimalOptions.roundingModes ?? toDecimalOptions.rm ?? rawOptions.roundingModes 45 | return Object.assign(rawOptions, { 46 | precision, 47 | callMethod, 48 | roundingModes, 49 | p: precision, 50 | cm: callMethod, 51 | rm: roundingModes, 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # 快速开始 6 | :::danger 7 | 使用前请注意,`AutoDecimal` 不会进行变量检测。当计算表达式中存在变量时,需要开发者自行评断该变量的值是否可以被 `decimal.js` 处理,如果 `decimal.js` 无法处理的话,会报错 `Uncaught Error: [DecimalError] Invalid argument`。 8 | 9 | 如果不想被 `AutoDecimal` 转换的话,可以通过[注释等方式跳过](./comment/index.md)。 10 | ::: 11 | ## 安装 12 | 13 | :::tip 14 | 插件依赖于 `decimal.js-light`,如果当前项目中已经使用了`decimal.js`、 `decimal.js-light`、 `big.js`, 其中任意一个,可以通过 `package` 来指定使用哪个库,不用重复安装。 15 | ::: 16 | 17 | :::code-group 18 | 19 | ```zsh [npm] 20 | npm install -D unplugin-auto-decimal 21 | # 已经安装过的可以跳过 22 | npm install -S decimal.js-light 23 | ``` 24 | 25 | ```zsh [pnpm] 26 | pnpm add -D unplugin-auto-decimal 27 | # 已经安装过的可以跳过 28 | pnpm add -S decimal.js-light 29 | ``` 30 | 31 | ```zsh [yarn] 32 | yarn add -D unplugin-auto-decimal 33 | # 已经安装过的可以跳过 34 | yarn add -S decimal.js-light 35 | ``` 36 | 37 | ::: 38 | 39 | ## 配置 40 | :::code-group 41 | 42 | ```ts [Vite] 43 | // vite.config.ts 44 | import AutoDecimal from 'unplugin-auto-decimal/vite' 45 | export default defineConfig({ 46 | plugins: [AutoDecimal({ 47 | /** options */ 48 | })] 49 | }) 50 | 51 | ``` 52 | 53 | ```js [Rspack] 54 | // rspack.config.ts 55 | const AutoDecimal = require('unplugin-auto-decimal/rspack') 56 | module.exports = { 57 | /* ... */ 58 | tools: { 59 | rspack: { 60 | plugins: [AutoDecimal( 61 | /** options */ 62 | )] 63 | } 64 | } 65 | } 66 | 67 | ``` 68 | 69 | ```js [Webpack] 70 | // webpack.config.js 71 | module.exports = { 72 | /* ... */ 73 | plugins: [require('unplugin-auto-decimal/webpack')({ 74 | /* options */ 75 | }), 76 | ], 77 | } 78 | 79 | ``` 80 | 81 | ```js [Vue-CLI] 82 | // vue.config.js 83 | module.exports = { 84 | configureWebpack: { 85 | plugins: [ 86 | require('unplugin-auto-decimal/webpack')({ 87 | /* options */ 88 | }), 89 | ], 90 | }, 91 | } 92 | 93 | ``` 94 | 95 | ::: 96 | :::warning 97 | 如果是 React 的话,必须将 `AutoDecimal` 放在 React 前面。 98 | ::: 99 | ```ts 100 | // vite.config.ts 101 | import AutoDecimal from 'vite-plugin-auto-decimal' 102 | import React from '@vitejs/plugin-react' 103 | export default defineConfig({ 104 | plugins: [AutoDecimal(), React()], 105 | }) 106 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import { groupIconMdPlugin, groupIconVitePlugin, localIconLoader } from 'vitepress-plugin-group-icons' 3 | import tag from './plugins/tag' 4 | // https://vitepress.dev/reference/site-config 5 | export default defineConfig({ 6 | head: [[ 7 | 'link', 8 | { rel: 'icon', href: '/unplugin-auto-decimal/favicon.svg' }, 9 | ]], 10 | title: 'AutoDecimal', 11 | description: 'A plugin that automatically converts basic operations in JavaScript to decimal.js methods', 12 | themeConfig: { 13 | // https://vitepress.dev/reference/default-theme-config 14 | nav: [ 15 | // { text: 'Home', link: '/' }, 16 | { text: '指南', link: '/guide/what-is-auto-decimal' }, 17 | ], 18 | logo: { 19 | light: '/logo.svg', 20 | dark: '/logo.svg', 21 | }, 22 | lastUpdated: { 23 | text: '最后更新于', 24 | }, 25 | 26 | sidebar: [ 27 | { 28 | text: '参考', 29 | items: [ 30 | { text: '什么是 AutoDecimal?', link: '/guide/what-is-auto-decimal' }, 31 | { text: '快速开始', link: '/guide/getting-started' }, 32 | { text: '配置选项', link: '/guide/api', items: [ 33 | { text: 'tailPatchZero', link: '/guide/api/tail-patch-zero' }, 34 | { text: 'supportString', link: '/guide/api/support-string' }, 35 | { text: 'toDecimal', link: '/guide/api/to-decimal' }, 36 | { text: 'supportNewFunction', link: '/guide/api/new-function' }, 37 | ] }, 38 | { text: '跳过转换', link: '/guide/comment', items: [ 39 | { text: 'splicing', link: '/guide/comment/splicing' }, 40 | { text: 'comment', link: '/guide/comment/ad-ignore' }, 41 | ] }, 42 | ], 43 | }, 44 | ], 45 | 46 | socialLinks: [ 47 | { icon: 'github', link: 'https://github.com/lyumg/unplugin-auto-decimal' }, 48 | ], 49 | }, 50 | base: '/unplugin-auto-decimal', 51 | lastUpdated: true, 52 | markdown: { 53 | config: (md) => { 54 | md.use(groupIconMdPlugin) 55 | md.use(tag) 56 | }, 57 | }, 58 | vite: { 59 | plugins: [ 60 | // @ts-expect-error plugins 61 | groupIconVitePlugin({ 62 | customIcon: { 63 | rspack: localIconLoader(import.meta.url, './assets/rspack.svg'), 64 | }, 65 | }), 66 | ], 67 | }, 68 | }) 69 | -------------------------------------------------------------------------------- /src/core/traverse/export-declaration.ts: -------------------------------------------------------------------------------- 1 | import type { NodePath } from '@babel/traverse' 2 | import type { ExportDefaultDeclaration, FunctionExpression, ObjectExpression } from '@babel/types' 3 | import type { Options } from '../../types' 4 | import { 5 | isBlockStatement, 6 | isCallExpression, 7 | isFunctionExpression, 8 | isIdentifier, 9 | isObjectExpression, 10 | isObjectMethod, 11 | isObjectProperty, 12 | isReturnStatement, 13 | isSpreadElement, 14 | } from '@babel/types' 15 | 16 | export function resolveExportDefaultDeclaration(path: NodePath, options: Options) { 17 | let { declaration } = path.node 18 | if (!isObjectExpression(declaration) && !isCallExpression(declaration)) 19 | return 20 | if (isCallExpression(declaration)) { 21 | const { arguments: args } = declaration 22 | const [objectExpr] = args 23 | if (!objectExpr || !isObjectExpression(objectExpr)) 24 | return 25 | declaration = objectExpr 26 | } 27 | const hasDataProperty = existDataProperty(declaration, options) 28 | if (!hasDataProperty) { 29 | const insertPosition = (declaration.start ?? 0) + 1 30 | const content = ` 31 | \n 32 | data() { 33 | this.${options.decimalPkgName} = ${options.decimalPkgName}; 34 | }, 35 | \n 36 | ` 37 | options.msa.prependLeft(insertPosition, content) 38 | } 39 | } 40 | 41 | function existDataProperty(declaration: ObjectExpression, options: Options) { 42 | const { properties } = declaration 43 | /** 44 | * 检查是否存在 data 函数, 仅支持 data 函数, 不支持 data 对象 45 | * export default { 46 | * data() {} 47 | * } 48 | */ 49 | return properties.some((prop) => { 50 | if (isSpreadElement(prop)) 51 | return false 52 | if (isObjectProperty(prop) && !isFunctionExpression(prop.value)) 53 | return false 54 | if (!isIdentifier(prop.key) || (isIdentifier(prop.key) && prop.key.name !== 'data')) 55 | return false 56 | 57 | const body = isObjectMethod(prop) ? prop.body : (prop.value as FunctionExpression).body 58 | if (!isBlockStatement(body)) 59 | return false 60 | 61 | const returnStatement = body.body.find(item => isReturnStatement(item)) 62 | if (!returnStatement) 63 | return false 64 | const content = `\nthis.${options.decimalPkgName} = ${options.decimalPkgName};\n` 65 | options.msa.prependLeft(returnStatement.start!, content) 66 | return true 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /test/tsx.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import fastGlob from 'fast-glob' 4 | import { describe, expect, it } from 'vitest' 5 | import { transform } from '../src/core/unplugin' 6 | 7 | describe('transform tsx', async () => { 8 | const root = resolve(__dirname, 'fixtures') 9 | const files = await fastGlob('*.tsx', { 10 | cwd: root, 11 | onlyFiles: true, 12 | }) 13 | for (const file of files) { 14 | const fixture = await fs.readFile(resolve(root, file), 'utf-8') 15 | const transformedCode = transform(fixture, file, { 16 | supportString: true, 17 | tailPatchZero: false, 18 | package: 'decimal.js-light', 19 | toDecimal: false, 20 | dts: false, 21 | decimalName: '__Decimal', 22 | supportNewFunction: false, 23 | })?.code ?? fixture 24 | it(` 25 | tsx normal 26 | input: 27 |
28 | output: 29 |
30 | input: 31 |
{0.1 + 0.2}
32 | output: 33 |
{new __Decimal(0.1).plus(0.2).toNumber()}
34 | `, () => { 35 | expect(transformedCode).toMatch('
') 36 | expect(transformedCode).toMatch('
{new __Decimal(0.1).plus(0.2).toNumber()}
') 37 | }) 38 | it(` 39 | tsx block-ad-ignore 40 | input: 41 | {0.1 + 0.2} 42 | output: 43 | {0.1 + 0.2} 44 | `, () => { 45 | expect(transformedCode).toMatch('{0.1 + 0.2}') 46 | }) 47 | it(` 48 | tsx next-ad-ignore 49 | input: 50 | {/* next-ad-ignore */} 51 |

{0.1 + 0.2}

52 | output: 53 |

{new __Decimal(0.1).plus(0.2).toNumber()}

54 | `, () => { 55 | expect(transformedCode).toMatch('

{new __Decimal(0.1).plus(0.2).toNumber()}

') 56 | }) 57 | it(` 58 | tsx next-ad-ignore multiple 59 | input: 60 | {/* next-ad-ignore */} 61 | skip: {0.1 + 0.2} transform: {1 - 0.9} 62 | output: 63 | skip: {0.1 + 0.2} transform: {1 - 0.9} 64 | `, () => { 65 | expect(transformedCode).toMatch('skip: {0.1 + 0.2} transform: {1 - 0.9}') 66 | }) 67 | } 68 | }) 69 | -------------------------------------------------------------------------------- /examples/vite-react/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/to-decimal.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import fastGlob from 'fast-glob' 4 | import { describe, expect, it } from 'vitest' 5 | import { transform } from '../src/core/unplugin' 6 | 7 | describe('transform', async () => { 8 | const root = resolve(__dirname, 'fixtures') 9 | const files = await fastGlob('to-decimal.ts', { 10 | cwd: root, 11 | onlyFiles: true, 12 | }) 13 | for (const file of files) { 14 | const fixture = await fs.readFile(resolve(root, file), 'utf-8') 15 | const transformedCode = transform(fixture, file, { 16 | supportString: false, 17 | tailPatchZero: false, 18 | package: 'decimal.js-light', 19 | toDecimal: true, 20 | dts: false, 21 | decimalName: '__Decimal', 22 | supportNewFunction: false, 23 | })?.code ?? fixture 24 | it(` 25 | ts 26 | input: 27 | const _a = 0.1 + 0.2.toDecimal() 28 | output: 29 | const _a = new __Decimal(0.1).plus(0.2).toNumber() 30 | `, () => { 31 | expect(transformedCode).toMatch('const _a = new __Decimal(0.1).plus(0.2).toNumber()') 32 | }) 33 | it(` 34 | ts _test() 35 | input: 36 | function _test() { 37 | const _ad = 0.111 + 0.222.toDecimal({ precision: 3, callMethod: 'toFixed' }) 38 | } 39 | output: 40 | function _test() { 41 | const _ad = new __Decimal(0.111).plus(0.222).toFixed(3, 4) 42 | } 43 | `, () => { 44 | expect(transformedCode).toMatch('const _ad = new __Decimal(0.111).plus(0.222).toFixed(3, 4)') 45 | }) 46 | it(` 47 | ts Class 48 | input: 49 | constructor() { 50 | this.block = 0.1 + 0.2.toDecimal() 51 | } 52 | output: 53 | constructor() { 54 | this.block = new __Decimal(0.1).plus(0.2).toNumber() 55 | } 56 | `, () => { 57 | expect(transformedCode).toMatch('this.block = new __Decimal(0.1).plus(0.2).toNumber()') 58 | }) 59 | it(` 60 | ts Array 61 | input: 62 | const _arr = [0, 0.1 + 0.2.toDecimal({ callMethod: 'toString' }), 3] 63 | output: 64 | const _arr = [0, new __Decimal(0.1).plus(0.2).toString(), 3] 65 | `, () => { 66 | expect(transformedCode).toMatch('const _arr = [0, new __Decimal(0.1).plus(0.2).toString(), 3]') 67 | }) 68 | it(` 69 | ts return decimal 70 | input: 71 | const _toDecimal = (0.111 + 0.222).toDecimal({ callMethod: 'decimal' }) 72 | output: 73 | const _toDecimal = new __Decimal(0.111).plus(0.222) 74 | `, () => { 75 | expect(transformedCode).toMatch('const _toDecimal = new __Decimal(0.111).plus(0.222)') 76 | }) 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /test/new-function-to-decimal.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import fastGlob from 'fast-glob' 4 | import { describe, expect, it } from 'vitest' 5 | import { resolveOptions } from '../src/core/options' 6 | import { transform } from '../src/core/unplugin' 7 | 8 | describe('transform', async () => { 9 | const root = resolve(__dirname, 'fixtures') 10 | const files = await fastGlob('new-function-to-decimal.ts', { 11 | cwd: root, 12 | onlyFiles: true, 13 | }) 14 | for (const file of files) { 15 | const fixture = await fs.readFile(resolve(root, file), 'utf-8') 16 | const transformedCode = transform(fixture, file, resolveOptions({ 17 | supportString: true, 18 | tailPatchZero: false, 19 | package: 'decimal.js-light', 20 | toDecimal: true, 21 | dts: false, 22 | decimalName: '__Decimal', 23 | supportNewFunction: true, 24 | }))?.code ?? fixture 25 | it(` 26 | new Function toDecimal 27 | input: 28 | const returnedValue = () => 'return a + b + 3' 29 | output: 30 | const returnedValue = () => 'return a + b + 3' 31 | `, () => { 32 | expect(transformedCode).toMatch(`const returnedValue = () => 'return a + b + 3'`) 33 | }) 34 | it(` 35 | new Function toDecimal 36 | input: 37 | return 'return a + b + 4..toDecimal()' 38 | output: 39 | return 'return new __Decimal(a).plus(b).plus(4.).toNumber()' 40 | `, () => { 41 | expect(transformedCode).toMatch(`return 'return new __Decimal(a).plus(b).plus(4.).toNumber()'`) 42 | }) 43 | it(` 44 | new Function Array 45 | input: 46 | const arr = [1, new Function('a', 'b', params)] 47 | arr[1](0.1, 0.2) 48 | output: 49 | const arr = [1, new Function('a', 'b', '__Decimal', params)] 50 | arr[1](0.1, 0.2, __Decimal) 51 | `, () => { 52 | expect(transformedCode).toMatch(`const arr = [1, new Function('a', 'b', '__Decimal', params)]`) 53 | expect(transformedCode).toMatch(`arr[1](0.1, 0.2, __Decimal)`) 54 | }) 55 | it(` 56 | new Function Object 57 | input: 58 | const obj = { b: new Function('a', 'b', params) } 59 | obj.b(num, 0.2)) 60 | output: 61 | const obj = { b: new Function('a', 'b', '__Decimal', params) } 62 | obj.b(num, 0.2, __Decimal)) 63 | `, () => { 64 | expect(transformedCode).toMatch(`const obj = { b: new Function('a', 'b', '__Decimal', params) }`) 65 | expect(transformedCode).toMatch(`obj.b(num, 0.2, __Decimal))`) 66 | }) 67 | it(` 68 | new Function Call Function 69 | input: 70 | const callFn = fn(num, 0.2) 71 | output: 72 | const callFn = fn(num, 0.2, __Decimal) 73 | `, () => { 74 | expect(transformedCode).toMatch(`const callFn = fn(num, 0.2, __Decimal)`) 75 | }) 76 | } 77 | }) 78 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { NodePath } from '@babel/traverse' 2 | import type { MagicStringAST } from 'magic-string-ast' 3 | import type { BIG_RM, DECIMAL_RM, DECIMAL_RM_LIGHT } from './core/constant' 4 | 5 | export interface AutoDecimal {} 6 | export interface Options { 7 | shouldSkip: boolean 8 | msa: MagicStringAST 9 | imported: boolean 10 | decimalPkgName: string 11 | initial: boolean 12 | callMethod: CallMethod 13 | callArgs: string 14 | autoDecimalOptions: InnerAutoDecimalOptions 15 | integer: boolean 16 | fromNewFunction?: boolean 17 | needImport?: boolean 18 | ownerPath?: NodePath 19 | } 20 | export interface ToDecimalConfig extends ToDecimalOptions { 21 | name?: string 22 | } 23 | export interface AutoDecimalOptions { 24 | supportString?: boolean 25 | tailPatchZero?: boolean 26 | package?: Package 27 | toDecimal?: boolean | ToDecimalConfig 28 | dts?: boolean | string 29 | decimalName?: string 30 | supportNewFunction?: boolean | NewFunctionOptions 31 | } 32 | export type InnerAutoDecimalOptions = Required 33 | export interface ToDecimalOptions { 34 | callMethod?: CallMethod 35 | /** callMethod */ 36 | cm?: CallMethod 37 | precision?: number 38 | /** precision */ 39 | p?: number 40 | roundingModes?: RoundingModes | number 41 | /** roundingModes */ 42 | rm?: RoundingModes | number 43 | } 44 | export type InnerToDecimalOptions = Required 45 | export type ToDecimal = (options?: T) => ToDecimalReturn 46 | export interface Extra { 47 | __extra: Record 48 | options: Options 49 | __shouldTransform: boolean 50 | } 51 | 52 | export type CallMethod = 'toNumber' | 'toString' | 'toFixed' | 'decimal' 53 | export type Package = 'decimal.js' | 'decimal.js-light' | 'big.js' 54 | export type ToDecimalReturn = GetToDecimalReturn | GetToDecimalReturn 55 | // @ts-expect-error support extend 56 | export type RoundingModes = AutoDecimal['package'] extends 'big.js' 57 | ? BigRoundingMode 58 | // @ts-expect-error support extend 59 | : AutoDecimal['package'] extends 'decimal.js' 60 | ? DecimalRoundingMode 61 | : DecimalLightRoundingMode 62 | export type DecimalRoundingMode = keyof typeof DECIMAL_RM 63 | export type DecimalLightRoundingMode = keyof typeof DECIMAL_RM_LIGHT 64 | export type BigRoundingMode = keyof typeof BIG_RM 65 | export type Operator = '+' | '-' | '*' | '/' 66 | export interface CommentState { 67 | line: number 68 | block: boolean 69 | next: boolean 70 | } 71 | export interface NewFunctionOptions { 72 | toDecimal?: boolean | ToDecimalConfig 73 | injectWindow?: string 74 | } 75 | type GetToDecimalReturn = V extends keyof T 76 | ? T[V] extends 'toFixed' | 'toString' 77 | ? string 78 | : T[V] extends 'decimal' 79 | // @ts-expect-error support extend interface 80 | ? AutoDecimal['decimal'] 81 | : number 82 | : never 83 | -------------------------------------------------------------------------------- /src/core/traverse/ast.ts: -------------------------------------------------------------------------------- 1 | import type { TraverseOptions } from '@babel/traverse' 2 | import type { File } from '@babel/types' 3 | import type { Options } from '../../types' 4 | import { isJSXEmptyExpression } from '@babel/types' 5 | import { BLOCK_COMMENT, FILE_COMMENT, PKG_NAME } from '../constant' 6 | import { resolveBinaryExpression } from './binary-expression' 7 | import { resolveCallExpression } from './call-expression' 8 | import { blockComment, innerComment, nextComment } from './comment' 9 | import { resolveExportDefaultDeclaration } from './export-declaration' 10 | import { resolveImportDeclaration } from './import-declaration' 11 | import { resolveNewFunctionExpression } from './new-function' 12 | 13 | export function traverseAst(options: Options, checkImport = true, templateImport = false): TraverseOptions { 14 | return { 15 | enter(path) { 16 | switch (path.type) { 17 | case 'Program': 18 | case 'ImportDeclaration': 19 | case 'ExportDefaultDeclaration': 20 | case 'JSXElement': 21 | case 'JSXOpeningElement': 22 | case 'JSXExpressionContainer': 23 | case 'BinaryExpression': 24 | break 25 | default: 26 | blockComment(path) 27 | nextComment(path) 28 | break 29 | } 30 | }, 31 | Program: { 32 | enter(path) { 33 | const file = path.parent as File 34 | const fileIgnore = file.comments?.some(comment => comment.value.includes(FILE_COMMENT)) ?? false 35 | options.imported = fileIgnore && templateImport 36 | if (fileIgnore && !templateImport) { 37 | path.skip() 38 | } 39 | }, 40 | exit() { 41 | const hasChanged = options.msa.hasChanged() 42 | if (!checkImport || options.imported || (!hasChanged && !templateImport)) { 43 | return 44 | } 45 | if (!options.needImport) 46 | return 47 | const pkgName = options.autoDecimalOptions?.package ?? PKG_NAME 48 | options.imported = true 49 | options.msa.prepend(`\nimport ${options.decimalPkgName} from '${pkgName}';\n`) 50 | }, 51 | }, 52 | ExportDefaultDeclaration(path) { 53 | if (!templateImport) 54 | return 55 | resolveExportDefaultDeclaration(path, options) 56 | }, 57 | ImportDeclaration(path) { 58 | if (options.imported) 59 | return 60 | resolveImportDeclaration(path, options) 61 | }, 62 | JSXElement: path => innerComment(path, BLOCK_COMMENT), 63 | JSXOpeningElement: (path) => { 64 | if (!path.node.attributes.length) 65 | return 66 | innerComment(path) 67 | }, 68 | JSXExpressionContainer: (path) => { 69 | if (isJSXEmptyExpression(path.node.expression)) 70 | return 71 | innerComment(path) 72 | }, 73 | BinaryExpression: path => resolveBinaryExpression(path, options), 74 | CallExpression: path => resolveCallExpression(path, options), 75 | NewExpression: path => resolveNewFunctionExpression(path, options), 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/new-function.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import fastGlob from 'fast-glob' 4 | import { describe, expect, it } from 'vitest' 5 | import { transform } from '../src/core/unplugin' 6 | 7 | describe('transform', async () => { 8 | const root = resolve(__dirname, 'fixtures') 9 | const files = await fastGlob('*-function.ts', { 10 | cwd: root, 11 | onlyFiles: true, 12 | }) 13 | for (const file of files) { 14 | const fixture = await fs.readFile(resolve(root, file), 'utf-8') 15 | const transformedCode = transform(fixture, file, { 16 | supportString: true, 17 | tailPatchZero: false, 18 | package: 'decimal.js-light', 19 | toDecimal: false, 20 | dts: false, 21 | decimalName: '__Decimal', 22 | supportNewFunction: true, 23 | })?.code ?? fixture 24 | it(` 25 | new Function return value 26 | input: 27 | const fn = new Function('a', 'b', \`return a + b\`) 28 | const result = fn(0.1, 0.2) 29 | output: 30 | const fn = new Function('a', 'b', '__Decimal', \`return new __Decimal(a).plus(b).toNumber()\`) 31 | const result = fn(0.1, 0.2, __Decimal) 32 | `, () => { 33 | expect(transformedCode).toMatch(`const fn = new Function('a', 'b', '__Decimal', \`return new __Decimal(a).plus(b).toNumber()\`)`) 34 | expect(transformedCode).toMatch(`const result = fn(0.1, 0.2, __Decimal)`) 35 | }) 36 | it(` 37 | new Function params 38 | input: 39 | const result = fn(0.1, 0.2) 40 | output: 41 | const result = fn(0.1, 0.2, __Decimal) 42 | `, () => { 43 | expect(transformedCode).toMatch('const result = fn(0.1, 0.2, __Decimal)') 44 | }) 45 | it(` 46 | new Function Array 47 | input: 48 | const arr = [1, new Function('a', 'b', params)] 49 | arr[1](0.1, 0.2) 50 | output: 51 | const arr = [1, new Function('a', 'b', '__Decimal', params)] 52 | arr[1](0.1, 0.2, __Decimal) 53 | `, () => { 54 | expect(transformedCode).toMatch(`const arr = [1, new Function('a', 'b', '__Decimal', params)]`) 55 | expect(transformedCode).toMatch(`arr[1](0.1, 0.2, __Decimal)`) 56 | }) 57 | it(` 58 | new Function Object 59 | input: 60 | const obj = { b: new Function('a', 'b', params) } 61 | obj.b(num, 0.2)) 62 | output: 63 | const obj = { b: new Function('a', 'b', '__Decimal', params) } 64 | obj.b(num, 0.2, __Decimal)) 65 | `, () => { 66 | expect(transformedCode).toMatch(`const obj = { b: new Function('a', 'b', '__Decimal', params) }`) 67 | expect(transformedCode).toMatch(`obj.b(num, 0.2, __Decimal))`) 68 | }) 69 | it(` 70 | new Function Call Function 71 | input: 72 | const callFn = fn(num, 0.2) 73 | output: 74 | const callFn = fn(num, 0.2, __Decimal) 75 | `, () => { 76 | expect(transformedCode).toMatch(`const callFn = fn(num, 0.2, __Decimal)`) 77 | }) 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /src/core/traverse/comment.ts: -------------------------------------------------------------------------------- 1 | import type { NodePath } from '@babel/traverse' 2 | import type { Comment } from '@babel/types' 3 | import { isJSXElement, isJSXEmptyExpression, isJSXExpressionContainer, isJSXOpeningElement } from '@babel/types' 4 | import { BASE_COMMENT, BLOCK_COMMENT, NEXT_COMMENT } from '../constant' 5 | 6 | export function getComments(path: NodePath) { 7 | const leadingComments = path.node.leadingComments ?? [] 8 | const trailingComments = path.node.trailingComments ?? [] 9 | const currentLineComments = trailingComments.filter((comment) => { 10 | return comment.loc?.start.line === path.node.loc?.start.line && comment.value.includes(BASE_COMMENT) 11 | }) 12 | return [...leadingComments, ...currentLineComments] 13 | } 14 | export function blockComment(path: NodePath) { 15 | skipAutoDecimalComment(path, BLOCK_COMMENT) 16 | } 17 | export function nextComment(path: NodePath) { 18 | skipAutoDecimalComment(path, NEXT_COMMENT) 19 | } 20 | export function innerComment(path: NodePath, igc: string | string[] = [NEXT_COMMENT, BLOCK_COMMENT]) { 21 | skipAutoDecimalComment(path, igc, true) 22 | } 23 | function skipAutoDecimalComment(path: NodePath, igc: string | string[], isJSX = false) { 24 | let comments: Comment[] | undefined 25 | let startLine = -1 26 | const rawPath = path 27 | if (isJSX) { 28 | if (isJSXOpeningElement(path.node)) { 29 | path = path.parentPath! 30 | startLine = path.node.loc?.start.line ?? -1 31 | } 32 | else if (isJSXExpressionContainer(path.node)) { 33 | startLine = path.node.expression.loc?.start.line ?? -1 34 | } 35 | let prevPath = path.getPrevSibling() 36 | if (!prevPath.node) 37 | return 38 | while ((!isJSXExpressionContainer(prevPath.node) || !isJSXEmptyExpression(prevPath.node.expression)) || startLine !== -1) { 39 | if (startLine !== -1 && isJSXExpressionContainer(prevPath.node)) { 40 | if (isJSXEmptyExpression(prevPath.node.expression)) 41 | break 42 | const exprStartLine = prevPath.node.loc?.start.line ?? 0 43 | if (exprStartLine !== startLine) { 44 | startLine = 0 45 | break 46 | } 47 | } 48 | prevPath = prevPath.getPrevSibling() 49 | if (!prevPath.node || isJSXElement(prevPath.node)) { 50 | if (isJSXElement(prevPath.node)) { 51 | const jsxElementStartLine = prevPath.node.loc?.start.line ?? 0 52 | if (startLine === jsxElementStartLine) { 53 | continue 54 | } 55 | } 56 | return 57 | } 58 | } 59 | if (!isJSXExpressionContainer(prevPath.node)) 60 | return 61 | const { expression } = prevPath.node 62 | if (isJSXEmptyExpression(expression)) { 63 | comments = expression.innerComments ?? [] 64 | } 65 | } 66 | else { 67 | comments = getComments(path) 68 | } 69 | 70 | const ignoreComment = Array.isArray(igc) ? igc : [igc] 71 | if (!comments) 72 | return 73 | const isIgnore = comments.some(comment => ignoreComment.some(ig => comment.value.includes(ig))) ?? false 74 | if (isIgnore) { 75 | rawPath.skip() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/new-function-inject-window.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import fastGlob from 'fast-glob' 4 | import { describe, expect, it } from 'vitest' 5 | import { transform } from '../src/core/unplugin' 6 | 7 | describe('transform', async () => { 8 | const root = resolve(__dirname, 'fixtures') 9 | const files = await fastGlob('*-inject-window.ts', { 10 | cwd: root, 11 | onlyFiles: true, 12 | }) 13 | for (const file of files) { 14 | const fixture = await fs.readFile(resolve(root, file), 'utf-8') 15 | const transformedCode = transform(fixture, file, { 16 | supportString: true, 17 | tailPatchZero: false, 18 | package: 'decimal.js-light', 19 | toDecimal: false, 20 | dts: false, 21 | decimalName: '__Decimal', 22 | supportNewFunction: { 23 | injectWindow: '$Dm', 24 | }, 25 | })?.code ?? fixture 26 | it(` 27 | new Function return value 28 | input: 29 | const fn = new Function('a', 'b', \`return a + b\`) 30 | const result = fn(0.1, 0.2) 31 | output: 32 | const fn = new Function('a', 'b', \`return new window.$Dm(a).plus(b).toNumber()\`) 33 | const result = fn(0.1, 0.2) 34 | `, () => { 35 | expect(transformedCode).toMatch(`const fn = new Function('a', 'b', \`return new window.$Dm(a).plus(b).toNumber()\`)`) 36 | expect(transformedCode).toMatch(`const result = fn(0.1, 0.2)`) 37 | }) 38 | it(` 39 | new Function params 40 | input: 41 | const result = fn(0.1, 0.2) 42 | output: 43 | const result = fn(0.1, 0.2) 44 | `, () => { 45 | expect(transformedCode).toMatch('const result = fn(0.1, 0.2)') 46 | }) 47 | it(` 48 | new Function Array 49 | input: 50 | const arr = [1, new Function('a', 'b', params)] 51 | arr[1](0.1, 0.2) 52 | output: 53 | const arr = [1, new Function('a', 'b', params)] 54 | arr[1](0.1, 0.2) 55 | `, () => { 56 | expect(transformedCode).toMatch(`const arr = [1, new Function('a', 'b', params)]`) 57 | expect(transformedCode).toMatch(`arr[1](0.1, 0.2)`) 58 | }) 59 | it(` 60 | new Function Object 61 | input: 62 | const obj = { b: new Function('a', 'b', params) } 63 | obj.b(num, 0.2)) 64 | output: 65 | const obj = { b: new Function('a', 'b', params) } 66 | obj.b(num, 0.2)) 67 | `, () => { 68 | expect(transformedCode).toMatch(`const obj = { b: new Function('a', 'b', params) }`) 69 | expect(transformedCode).toMatch(`obj.b(num, 0.2))`) 70 | }) 71 | it(` 72 | new Function Call Function 73 | input: 74 | const callFn = fn(num, 0.2) 75 | output: 76 | const callFn = fn(num, 0.2) 77 | `, () => { 78 | expect(transformedCode).toMatch(`const callFn = fn(num, 0.2)`) 79 | }) 80 | it(` 81 | new Function Call Function Inject Window 82 | input: 83 | return a + b + 3 84 | output: 85 | return new window.$Dm(a).plus(b).plus(3).toNumber() 86 | `, () => { 87 | expect(transformedCode).toMatch(`return new window.$Dm(a).plus(b).plus(3).toNumber()`) 88 | }) 89 | } 90 | }) 91 | -------------------------------------------------------------------------------- /src/core/traverse/call-expression.ts: -------------------------------------------------------------------------------- 1 | import type { NodePath } from '@babel/traverse' 2 | import type { BinaryExpression, CallExpression, NewExpression } from '@babel/types' 3 | import type { InnerToDecimalOptions, Options } from '../../types' 4 | import { isBinaryExpression, isIdentifier, isMemberExpression, isObjectExpression } from '@babel/types' 5 | import { processBinary, resolveNewFunctionExpression } from '.' 6 | import { DEFAULT_TO_DECIMAL_CONFIG } from '../constant' 7 | import { mergeToDecimalOptions } from '../options' 8 | import { getRootBinaryExprPath, getRoundingMode } from '../utils' 9 | 10 | export function resolveCallExpression(path: NodePath, options: Options) { 11 | const { autoDecimalOptions } = options 12 | const { toDecimal, supportNewFunction } = autoDecimalOptions 13 | if (!toDecimal && !supportNewFunction) 14 | return 15 | const { node } = path 16 | const { callee, arguments: args } = node 17 | if (supportNewFunction && isIdentifier(callee) && callee.name === 'Function') { 18 | resolveNewFunctionExpression(path as unknown as NodePath, options) 19 | return 20 | } 21 | if (!isMemberExpression(callee)) 22 | return 23 | const toDecimalOptions: InnerToDecimalOptions = { ...DEFAULT_TO_DECIMAL_CONFIG } 24 | if (toDecimal) { 25 | mergeToDecimalOptions(toDecimalOptions, toDecimal) 26 | } 27 | const { property, object } = callee 28 | if (!isIdentifier(property) || property.name !== toDecimalOptions.name) 29 | return 30 | if (!isBinaryExpression(path.parentPath.node) && !isBinaryExpression(object)) { 31 | throw new SyntaxError(` 32 | line: ${path.parentPath.node.loc?.start.line}, ${options.msa.sliceNode(path.parentPath.node).toString()} 或 ${options.msa.sliceNode(object).toString()} 不是有效的计算表达式 33 | `) 34 | } 35 | if (args && args.length > 0) { 36 | const [arg] = args 37 | if (!isObjectExpression(arg)) { 38 | throw new TypeError('toDecimal 参数错误') 39 | } 40 | const rawArg = options.msa.snipNode(arg).toString() 41 | const jsonArg = rawArg.replace(/(\w+):/g, '"$1":').replace(/'/g, '"') 42 | try { 43 | const argToDecimalOptions = JSON.parse(jsonArg) 44 | mergeToDecimalOptions(toDecimalOptions, argToDecimalOptions) 45 | } 46 | catch (e: unknown) { 47 | console.error(e) 48 | } 49 | } 50 | let callArgs = '()' 51 | if (toDecimalOptions.callMethod === 'toFixed') { 52 | callArgs = `(${toDecimalOptions.precision}, ${getRoundingMode(toDecimalOptions.roundingModes, autoDecimalOptions.package)})` 53 | } 54 | const start = object.end ?? 0 55 | options.msa.remove(start, node.end ?? 0) 56 | const resolveBinaryOptions = { 57 | ...options, 58 | initial: true, 59 | callArgs, 60 | callMethod: toDecimalOptions.callMethod, 61 | } 62 | if (isBinaryExpression(object)) { 63 | if (object.start !== node.start) { 64 | options.msa.remove(node.start ?? 0, object.start ?? 0) 65 | } 66 | object.extra = { 67 | ...object.extra, 68 | __extra: object.extra, 69 | options: resolveBinaryOptions, 70 | __shouldTransform: true, 71 | } 72 | return 73 | } 74 | const rootPath = getRootBinaryExprPath(path) 75 | const runtimeOptions = {} as Options 76 | processBinary(Object.assign(runtimeOptions, resolveBinaryOptions), rootPath as NodePath) 77 | Object.assign(options, { needImport: runtimeOptions.needImport }) 78 | } 79 | -------------------------------------------------------------------------------- /docs/guide/api/new-function.md: -------------------------------------------------------------------------------- 1 | # 支持 new Function ^(1.4.0) 2 | 3 | 默认情况下,`AutoDecimal` 仅会处理计算表达式,当启用了 `supportString` 属性时,也仅仅会处理计算表达式中可以被转换为数字的字符串。 4 | 5 | ```ts 6 | // AutoDecimal 默认参数下 7 | const a = 0.1 8 | const c = a + '0.2' 9 | console.log(c) // "0.10.2" 10 | ``` 11 | 12 | 当 `supportString` 为 true 时 13 | ```ts 14 | const a = 0.1 15 | // 当启用 `supportString` 后,由于 '0.2' 可以被转换为数字,所以计算结果为 0.3 16 | const c = a + '0.2' 17 | console.log(c) // 0.3 18 | // 由于 'b' 不能转换为数字,所以结果为 '0.1b' 19 | const d = a + 'b' 20 | console.log(d) // '0.1b' 21 | ``` 22 | 23 | 但是下面的却不会进行转换 24 | ```ts 25 | const fn = new Function('a', 'b', 'return a + b') 26 | const result = fn(0.1, 0.2) 27 | console.log(result) // 0.30000000000000004 28 | ``` 29 | 因为 `fn` 是通过 `new Function` 创建的函数,而需要转换的 `return a + b`,是一个字符串,且 `AutoDecimal` 仅处理计算表达式,不会处理单个字符串。所以 `new Function` 中的字符串,会跳过。 30 | 31 | 那么如果想要 `AutoDecimal` 能够处理 `new Function` 中的字符串时,要怎么办呢。 32 | 33 | 34 | ## 配置项 35 | | 属性 | 描述 | 类型 | 默认值 | 36 | | :----------------: | :-------------------: | :------: |:------: | 37 | | toDecimal | 默认继承 [`toDecimal`](./to-decimal.md) 参数,如果设置此参数,则优先使用此参数 | ToDecimalConfig | - | 38 | | injectWindow ^(1.4.3) | 将 `Decimal` 挂载到 `window` 中的属性名称 | string | - | 39 | 40 | 41 | ## supportNewFunction 42 | :::code-group 43 | ```ts [vite.config.ts] {10} 44 | export default defineConfig({ 45 | plugins: [ 46 | AutoDecimal({ 47 | /** 48 | * supportNewFunction: { 49 | * toDecimal?: boolean 50 | * injectWindow?: string 51 | * } 52 | */ 53 | supportNewFunction: true 54 | }) 55 | ] 56 | }) 57 | ``` 58 | ::: 59 | 60 | 此时, `AutoDecimal` 就会解析 `new Function` 中的 'return a + b' 了。 61 | 62 | ```ts 63 | const fn = new Function('a', 'b', 'return a + b') 64 | const result = fn(0.1, 0.2) 65 | console.log(result) // 0.3 66 | ``` 67 | 68 | ### supportNewFunction.toDecimal 69 | 此属性可以告诉 `AutoDecimal` 在处理 `new Function` 时,是否需要跟随 [`toDecimal`](./to-decimal.md)的设定来进行处理。 70 | 当启用了 `toDecimal` 后,可以通过 `supportNewFunction.toDecimal` 来单独启用、停用或者修改 `toDecimal` 的设定。 71 | 72 | :::code-group 73 | ```ts [vite.config.ts] {5} 74 | export default defineConfig({ 75 | plugins: [ 76 | AutoDecimal({ 77 | toDecimal: true, 78 | supportNewFunction: { 79 | // 当这里设为 false 时,new Function 中的参数将不需要使用 toDecimal() 80 | toDecimal: false 81 | } 82 | }) 83 | ] 84 | }) 85 | ``` 86 | ::: 87 | 88 | 89 | :::tip 90 | 目前 `new Function` 支持的调用方式有限(目前所能想到的一些调用方式都已实现),它可以赋值给一个对象属性、数组中的某项、某个变量,但是一定不要太过于复杂,如果你遇到因为某些特殊的调用方式而造成的无法解析,或者无法得到正确的结果时,可以提[issues](https://github.com/lyumg/unplugin-auto-decimal/issues),我会第一时间解决。 91 | ::: 92 | 93 | ### supportNewFunction.injectWindow 94 | 由于在转换 `new Function` 时,`Decimal` 是通过参数注入的方式实现,需要查找 `new Function` 的定义、调用以及作用域等相关信息,费时费力。那么如果想 “肆意妄为” 的在 `new Function` 中使用 `Decimal`,要怎么办呢?可以先将 `decimal.js` 挂载到 `window` 上,然后通过 `injectWindow` 提供挂载的属性名称即可。 95 | 96 | 不使用 `injectWindow` 时,通过参数注入 `Decimal` 97 | ```ts {7,8} 98 | const fn = new Function('a', 'b', 'return a + b') 99 | const result = fn(0.1, 0.2) 100 | console.log(result) // 0.3 101 | 102 | // 上述代码会转换为 103 | import Decimal from 'decimal.js' 104 | const fn = new Function('a', 'b', 'Decimal', 'return new Decimal(a).plus(b).toNumber()') 105 | const result = fn(0.1, 0.2, Decimal) 106 | console.log(result) // 0.3 107 | ``` 108 | 109 | 使用 `injectWindow` 时,直接使用 `window[injectWindow]` 来调用 `Decimal` 110 | :::code-group 111 | ```ts [vite.config.ts] {5} 112 | export default defineConfig({ 113 | plugins: [ 114 | AutoDecimal({ 115 | supportNewFunction: { 116 | injectWindow: 'injectDecimal' 117 | } 118 | }) 119 | ] 120 | }) 121 | ``` 122 | ::: 123 | 124 | ```ts {6} 125 | const fn = new Function('a', 'b', 'return a + b') 126 | const result = fn(0.1, 0.2) 127 | console.log(result) // 0.3 128 | 129 | // 此时,上述代码会转换为 130 | const fn = new Function('a', 'b', 'return new window.injectDecimal(a).plus(b).toNumber()') 131 | const result = fn(0.1, 0.2) 132 | console.log(result) // 0.3 133 | ``` 134 | -------------------------------------------------------------------------------- /test/setup.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import fastGlob from 'fast-glob' 4 | import { describe, expect, it } from 'vitest' 5 | import { transform } from '../src/core/unplugin' 6 | 7 | describe('transform', async () => { 8 | const root = resolve(__dirname, 'fixtures') 9 | const files = await fastGlob('*setup.vue', { 10 | cwd: root, 11 | onlyFiles: true, 12 | }) 13 | for (const file of files) { 14 | const fixture = await fs.readFile(resolve(root, file), 'utf-8') 15 | const transformedCode = transform(fixture, file, { 16 | supportString: true, 17 | tailPatchZero: false, 18 | package: 'decimal.js-light', 19 | toDecimal: false, 20 | dts: false, 21 | decimalName: '__Decimal', 22 | supportNewFunction: false, 23 | })?.code ?? fixture 24 | 25 | it(` 26 | vue.setup 27 | input: 28 | const sum = ref(0.1 + 0.2) 29 | output: 30 | const sum = ref(new __Decimal(0.1).plus(0.2).toNumber()) 31 | `, () => { 32 | expect(transformedCode).toMatch('sum = ref(new __Decimal(0.1).plus(0.2).toNumber())') 33 | }) 34 | it(` 35 | vue.setup next-ad-ignore 36 | input: 37 | const obj = { 38 | // next-ad-ignore 39 | a: 0.1 + 0.2, 40 | } 41 | output: 42 | const obj = { 43 | // next-ad-ignore 44 | a: 0.1 + 0.2, 45 | } 46 | `, () => { 47 | expect(transformedCode).toMatch('a: 0.1 + 0.2') 48 | }) 49 | it(` 50 | vue.setup block-ad-ignore 51 | input: 52 | // block-ad-ignore 53 | function _test() { 54 | return 0.1 + 0.2 55 | } 56 | output: 57 | // block-ad-ignore 58 | function _test() { 59 | return 0.1 + 0.2 60 | } 61 | `, () => { 62 | expect(transformedCode).toMatch('return 0.1 + 0.2') 63 | }) 64 | it(` 65 | vue.setup template 66 | input: 67 |
transformed:{{ obj.a }} {{ 0.1 + 0.2 }}
68 | output: 69 |
transformed:{{ obj.a }} {{ new __Decimal(0.1).plus(0.2).toNumber() }}
70 | `, () => { 71 | expect(transformedCode).toMatch('
transformed:{{ obj.a }} {{ new __Decimal(0.1).plus(0.2).toNumber() }}
') 72 | }) 73 | it(` 74 | vue.setup template next-ad-ignore 75 | input: 76 | 77 | next-ad-ignore:{{ 0.1 + 0.2 }} 78 | output: 79 | 80 | next-ad-ignore:{{ 0.1 + 0.2 }} 81 | `, () => { 82 | expect(transformedCode).toMatch('next-ad-ignore:{{ 0.1 + 0.2 }}') 83 | }) 84 | it(` 85 | vue.setup template next-ad-ignore 86 | input: 87 | 88 |

89 | {{ 0.1 + 0.2 }} 90 |

91 | output: 92 | 93 |

94 | next-ad-ignore transform: {{ new __Decimal(0.1).plus(0.2).toNumber() }} 95 |

96 | `, () => { 97 | expect(transformedCode).toMatch('next-ad-ignore:{{ 0.1 + 0.2 }}') 98 | expect(transformedCode).toMatch('next-ad-ignore transform: {{ new __Decimal(0.1).plus(0.2).toNumber() }}') 99 | }) 100 | it(` 101 | vue.setup template next-ad-ignore multiple 102 | input: 103 | 104 | next-ad-ignore multiple:{{ 0.1 + 0.2 }} {{ 1 - 0.9 }} 105 | output: 106 | 107 | next-ad-ignore multiple:{{ 0.1 + 0.2 }} {{ 1 - 0.9 }} 108 | `, () => { 109 | expect(transformedCode).toMatch('next-ad-ignore multiple:{{ 0.1 + 0.2 }} {{ 1 - 0.9 }}') 110 | }) 111 | it(` 112 | vue.setup template next-ad-ignore multiple 113 | input: 114 | 115 | next-ad-ignore skip:{{ 0.1 + 0.2 }} 116 | next-ad-ignore transform: {{ 1 - 0.9 }} 117 | output: 118 | 119 | next-ad-ignore multiple skip:{{ 0.1 + 0.2 }} 120 | next-ad-ignore multiple transform: {{ new __Decimal(1).minus(0.9).toNumber() }} 121 | `, () => { 122 | expect(transformedCode).toMatch('next-ad-ignore multiple skip:{{ 0.1 + 0.2 }}') 123 | expect(transformedCode).toMatch('next-ad-ignore multiple transform: {{ new __Decimal(1).minus(0.9).toNumber() }}') 124 | }) 125 | it(` 126 | vue.setup template block-ad-ignore 127 | input: 128 | 129 |
130 | block-ad-ignore:{{ 11.2 + 24.4 + 66 / (0.1 + 0.2) }} 131 |
132 | output: 133 | 134 |
135 | block-ad-ignore:{{ 11.2 + 24.4 + 66 / (0.1 + 0.2) }} 136 |
137 | `, () => { 138 | expect(transformedCode).toMatch('block-ad-ignore:{{ 11.2 + 24.4 + 66 / (0.1 + 0.2) }}') 139 | }) 140 | } 141 | }) 142 | -------------------------------------------------------------------------------- /test/options.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import fastGlob from 'fast-glob' 4 | import { describe, expect, it } from 'vitest' 5 | import { transform } from '../src/core/unplugin' 6 | 7 | describe('transform', async () => { 8 | const root = resolve(__dirname, 'fixtures') 9 | const files = await fastGlob('*options.vue', { 10 | cwd: root, 11 | onlyFiles: true, 12 | }) 13 | for (const file of files) { 14 | const fixture = await fs.readFile(resolve(root, file), 'utf-8') 15 | const transformedCode = transform(fixture, file, { 16 | supportString: true, 17 | tailPatchZero: false, 18 | package: 'decimal.js-light', 19 | toDecimal: false, 20 | dts: false, 21 | decimalName: '__Decimal', 22 | supportNewFunction: false, 23 | })?.code ?? fixture 24 | it(` 25 | vue 26 | input: 27 | const _s = \`\${0.1 + 0.2}\` 28 | output: 29 | const _s = \`\${new __Decimal(0.1).plus(0.2).toNumber()}\` 30 | `, () => { 31 | // eslint-disable-next-line no-template-curly-in-string 32 | expect(transformedCode).toMatch('const _s = `${new __Decimal(0.1).plus(0.2).toNumber()}`') 33 | }) 34 | it(` 35 | vue next-ad-ignore 36 | input: 37 | // next-ad-ignore 38 | const _sum = 0.1 + 0.2 39 | output: 40 | // next-ad-ignore 41 | const _sum = 0.1 + 0.2 42 | `, () => { 43 | expect(transformedCode).toMatch('const _sum = 0.1 + 0.2') 44 | }) 45 | it(` 46 | vue block-ad-ignore 47 | input: 48 | // block-ad-ignore 49 | { 50 | const _a = 0.1 + 0.2 51 | const _obj = { a: 0.1 + 0.2 } 52 | } 53 | output: 54 | // block-ad-ignore 55 | { 56 | const _a = 0.1 + 0.2 57 | const _obj = { a: 0.1 + 0.2 } 58 | } 59 | `, () => { 60 | expect(transformedCode).toMatch('const _a = 0.1 + 0.2') 61 | expect(transformedCode).toMatch('const _obj = { a: 0.1 + 0.2 }') 62 | }) 63 | it(` 64 | vue template 65 | input: 66 |
67 | transform:{{ 0.1 + 0.2 }} b:{{ 1 - 0.9 }} 68 |
69 | output: 70 |
71 | transform:{{ new __Decimal(0.1).plus(0.2).toNumber() }} b:{{ new __Decimal(1).minus(0.9).toNumber() }} 72 |
73 | `, () => { 74 | expect(transformedCode).toMatch('
') 75 | expect(transformedCode).toMatch('transform:{{ new __Decimal(0.1).plus(0.2).toNumber() }} b:{{ new __Decimal(1).minus(0.9).toNumber() }}') 76 | }) 77 | it(` 78 | vue template next-ad-ignore 79 | input: 80 | 81 | next-ad-ignore transform:{{ 0.1 + 0.2 }} 82 | 83 | output: 84 | 85 | next-ad-ignore transform:{{ new __Decimal(0.1).plus(0.2).toNumber() }} 86 | 87 | `, () => { 88 | expect(transformedCode).toMatch('') 89 | expect(transformedCode).toMatch('next-ad-ignore transform:{{ new __Decimal(0.1).plus(0.2).toNumber() }}') 90 | }) 91 | it(` 92 | vue template next-ad-ignore 93 | input: 94 | 95 | 96 | next-ad-ignore :{{ 0.1 + 0.2 }} 97 | 98 | output: 99 | 100 | 101 | next-ad-ignore :{{ 0.1 + 0.2 }} 102 | 103 | `, () => { 104 | expect(transformedCode).toMatch('') 105 | expect(transformedCode).toMatch('next-ad-ignore:{{ 0.1 + 0.2 }}') 106 | }) 107 | it(` 108 | vue template next-ad-ignore multiple skip:{{ 0.1 + 0.2 }} {{ 0.2 + 0.1 }} 109 | input: 110 | 111 | multiple skip:{{ 0.1 + 0.2 }} {{ 0.2 + 0.1 }} 112 | multiple transform:{{ 1 - 0.9 }} 113 | output: 114 | 115 | multiple skip:{{ 0.1 + 0.2 }} {{ 0.2 + 0.1 }} 116 | multiple transform:{{ new __Decimal(1).minus(0.9).toNumber() }} 117 | `, () => { 118 | expect(transformedCode).toMatch('multiple skip:{{ 0.1 + 0.2 }} {{ 0.2 + 0.1 }}') 119 | expect(transformedCode).toMatch('multiple transform:{{ new __Decimal(1).minus(0.9).toNumber() }}') 120 | }) 121 | it(` 122 | vue template block-ad-ignore 123 | input: 124 | 125 |

126 | block-ad-ignore:{{ 0.1 + 0.2 }} 127 |

128 | output: 129 | 130 |

131 | block-ad-ignore:{{ 0.1 + 0.2 }} 132 |

133 | `, () => { 134 | expect(transformedCode).toMatch('

') 135 | expect(transformedCode).toMatch('block-ad-ignore:{{ 0.1 + 0.2 }}') 136 | }) 137 | } 138 | }) 139 | -------------------------------------------------------------------------------- /docs/guide/comment/ad-ignore.md: -------------------------------------------------------------------------------- 1 | # 添加相应注释 2 | 有时候,有些计算其实是不需要转换的。那么要如何跳过某个计算表达式或者都跳过呢? 3 | 4 | - 添加相应的注释(`jsx` 中需要注意, 在表达式中某些情况可能需要使用 JavaScript 注释) 5 | - 添加 `ad-ignore` prop 6 | - `supportString: true`时, 末尾拼接一个空字符串 7 | - `supportString: false`时, 末尾拼接任意字符串 8 | 9 | ## script 10 | 11 | 当你想跳过某个计算表达式,不需要转换时,可以使用 `next-ad-ignore`: 12 | 13 | ```ts 14 | // next-ad-ignore 15 | const igSum = 0.1 + 0.2; 16 | console.log('igSum => ', igSum); // 0.30000000000000004 17 | 18 | // 注释在右侧 19 | const igSumStrDirection = 0.1 + 0.2 // next-ad-ignore 20 | console.log('igSumStrDirection => ', igSumStrDirection); // '0.30000000000000004' 21 | 22 | // 末尾拼接一个空字符串 23 | const igSumStr = 0.1 + 0.2 + ''; 24 | console.log('igSumStr => ', igSumStr); // '0.30000000000000004' 25 | ``` 26 | 27 | 如果你想在某个作用域内,所有计算表达式都不进行转换的话,使用 `block-ad-ignore`: 28 | 29 | ```ts 30 | const sum = 0.1 + 0.2 31 | console.log('sum => ', sum) // 0.3 32 | 33 | const sumStr = 0.1 + 0.2 + '' 34 | console.log('sumStr => ', sumStr) // '0.30000000000000004' 35 | ... 36 | // block-ad-ignore 37 | { 38 | const igSum = 0.1 + 0.2 39 | console.log('igSum => ', igSum) // 0.30000000000000004 40 | 41 | const sum = 0.1 + 0.2 42 | console.log('sum => ', sum) // 0.30000000000000004 43 | } 44 | 45 | function sum() { 46 | const sum = 0.1 + 0.2 47 | console.log('sum => ', sum) // 0.3 48 | 49 | const sumStr = 0.1 + 0.2 + '' 50 | console.log('sum => ', sum) // '0.30000000000000004' 51 | } 52 | sum() 53 | 54 | // block-ad-ignore 55 | function igSumFn() { 56 | const igSum = 0.1 + 0.2 57 | console.log('sum => ', sum) // 0.30000000000000004 58 | } 59 | igSumFn() 60 | ``` 61 | 62 | 如果某个文件内的计算表达式都不需要转换的话,可以在文件顶部使用 `file-ad-ignore`: 63 | 64 | ```ts 65 | // file-ad-ignore 66 | ... 67 | const igSum = 0.1 + 0.2 68 | console.log('igSum => ', igSum) // 0.30000000000000004 69 | 70 | const igSum2 = 0.1 + 0.2 71 | console.log('igSum2 => ', igSum2) // 0.30000000000000004 72 | ``` 73 | 74 | ## vue template 75 | 76 | 如果想只禁用 `template` 内的计算表达式的话,在 `template` 标签添加 `ad-ignore` prop 即可。 77 | 78 | ```vue 79 | 80 | 83 | ``` 84 | 85 | `ad-ignore` 只影响在 `template` 中定义的计算表达式是否转换, 不会影响到 `script` 中定义的计算表达式。 86 | ```vue 87 | 96 | 99 | ``` 100 | 101 | 在 `template` 中, 可以使用 `next-ad-ignore` 和 `block-ad-ignore`,也需要区分两种注释。 102 | 103 | - `next-ad-ignore` 用于组件 `prop` 和绑定的各个参数, 但不包含插槽与子集 104 | - `block-ad-ignore` 用于控制整个组件的所有属性包括插槽及子集 105 | 106 | ```html 107 | 133 | 139 | ``` 140 | 141 | ## jsx 142 | 143 | ```tsx 144 | import OtherComponent from '..' 145 | render() { 146 | const list = Array.from({ length: 3 }, item => 0.1) 147 | return (

148 | { 149 | /* 150 | * next-ad-ignore 151 | * next-ad-ignore 不负责插槽和子集 152 | * 所以 title=0.30000000000000004 153 | * jsx comment: 0.3 154 | */ 155 | } 156 |
jsx comment: {0.1 + 0.2}
157 | 158 | {/* 0.30000000000000004 */} 159 |
拼接空字符串: {0.1 + 0.2 + ''}
160 | 161 | {/* 0.3 */} 162 |
正常输出: {0.1 + 0.2}
163 | { 164 | /** 165 | * block-ad-ignore 166 | * 组件中所有的属性都不会转换 167 | * num=0.30000000000000004 168 | */ 169 | } 170 | 171 | {/* slot 0.30000000000000004 */} 172 | {0.1 + 0.2} 173 | {/* num=0.30000000000000004 */} 174 | 175 | {/* slot 0.30000000000000004 */} 176 | {0.1 + 0.2} 177 | 178 | 179 | { 180 | list.map(item => { 181 | {/* 这里要注意使用 JavaScript 中的注释形式, 不能使用 jsx 中的注释形式 */} 182 | {/* 这里要注意使用 JavaScript 中的注释形式, 不能使用 jsx 中的注释形式 */} 183 | {/* 这里要注意使用 JavaScript 中的注释形式, 不能使用 jsx 中的注释形式 */} 184 | 185 | {/* 所以这种是不生效的 next-ad-ignore */} 186 | const sum = 0.1 + 0.2 187 | console.log('sum => ', sum) // 0.3 188 | // 这种是生效的 next-ad-ignore
0.30000000000000004
189 | return
{item + 0.2}
190 | }) 191 | } 192 |
) 193 | } 194 | ``` -------------------------------------------------------------------------------- /docs/guide/api/to-decimal.md: -------------------------------------------------------------------------------- 1 | # 显式转换 ^(1.2.0) 2 | 3 | :::tip 4 | `toDecimal` 启用后,所有的计算将不会进行转换,只有显式调用 `toDecimal` 才会将计算转换为 `Decimal` 方法。 5 | 同时,属性`supportString`, `tailPatchZero` 也将失效。 6 | ::: 7 | 8 | ## 配置项 9 | `toDecimal` 配置项可以全局配置,也可以在调用时配置。调用时的配置项优先于全局配置。 10 | 11 | :::warning 12 | 下述所有属性的值,仅支持字面量等具体值,不支持变量。 13 | 14 | 如想使用变量的话,可以通过 `callMethod: 'decimal'` 得到 `Decimal` 实例,然后通过 `Decimal` 实例来调用对应的方法来传入变量 15 | ::: 16 | 17 | | 属性 | 描述 | 类型 | 默认值 | 18 | | ---------------- | :-------------------: | :------: |:------: | 19 | | [callMethod](#todecimal-callmethod) | 转换为 `Decimal` 后,调用的方法。值为 `decimal` 时,会返回 `Decimal` 实例 | toNumber \| toString \| toFixed \| decimal ^(1.3.0) | toNumber | 20 | | [precision](#todecimal-precision) | 保留的小数精度, 仅当 `callMethod` 为 toFixed 有效 | number | 2 | 21 | | [roundingModes](#todecimal-roundingmodes) | `Decimal` 的 roundingModes, 仅当 `callMethod` 为 toFixed 有效 | number \| ROUNDING_MODES | ROUND_HALF_UP | 22 | | [name](#todecimal-name) | 用于自定义转换 `Decimal`时,匹配的函数名称,**仅配置插件时可用** | string | toDecimal | 23 | :::tip 24 | 为了节省一点大家的宝贵时间,上述的属性提供了缩写: 25 | > cm -> callMethod 26 | 27 | > p -> precision 28 | 29 | > rm -> roundingModes 30 | ::: 31 | 32 | ## TypeScript 支持 33 | 34 | :::code-group 35 | 36 | ```ts [vite.config.ts] 37 | export default defineConfig({ 38 | plugins: [ 39 | AutoDecimal({ 40 | toDecimal: true, 41 | // 默认在根目录生成一个 'auto-decimal.d.ts' 文件 42 | dts: true 43 | }) 44 | ] 45 | }) 46 | ``` 47 | ::: 48 | 49 | :::code-group 50 | 51 | ```json [tsconfig.json] 52 | { 53 | "compilerOptions": { 54 | // ... 55 | "types": ["./auto-decimal.d.ts" /* ... */] 56 | } 57 | } 58 | ``` 59 | ::: 60 | 61 | :::warning 62 | 如果更改了 `package` 配置,想要 `roundingModes` 给予足够正确的提示,需要在项目中创建一个 d.ts 文件,并且写入相应的 `package`。 63 | 64 | 同时,如果想要返回 `Decimal` 时得到相应的类型,需要指定 `decimal` 65 | ::: 66 | ```ts 67 | export {} 68 | declare module 'unplugin-auto-decimal/types' { 69 | interface AutoDecimal{ 70 | // 填写对应的 package 即可 71 | package: 'big.js' 72 | // callMethod: 'decimal' 时的类型 73 | decimal: import('decimal.js-light').Decimal 74 | } 75 | } 76 | ``` 77 | 78 | ## 使用 79 | :::code-group 80 | ```ts [vite.config.ts] 81 | export default defineConfig({ 82 | plugins: [ 83 | AutoDecimal({ 84 | // toDecimal: true 85 | toDecimal: { 86 | callMethod: 'toNumber', 87 | precision: 2, 88 | roundingModes: 'ROUND_HALF_UP', 89 | name: 'toDecimal' 90 | } 91 | }) 92 | ] 93 | }) 94 | ``` 95 | ::: 96 | ```ts 97 | const a = 0.1 + 0.2 98 | console.log(a, '0.30000000000000004') 99 | 100 | const b = 0.1 + 0.2.toDecimal() 101 | console.log(b, 0.3) 102 | 103 | const c = 0.1111 + 0.2222.toDecimal({precision: 3, callMethod: 'toFixed', roundingModes: 'ROUND_UP'}) 104 | console.log(c, "0.334") 105 | // 使用默认配置 106 | const d = 0.1111 + 0.2222.toDecimal() 107 | console.log(d, 0.3333) 108 | ``` 109 | 110 | 如果感觉上面的使用方法有些莫名其妙,也可以将其用括号包裹后,在调用 `toDecimal`。 111 | ```ts 112 | const a = 0.1 + 0.2 113 | console.log(a, '0.30000000000000004') 114 | 115 | const b = (0.1 + 0.2).toDecimal() 116 | console.log(b, 0.3) 117 | 118 | const c = (0.1111 + 0.2222).toDecimal({precision: 3, callMethod: 'toFixed', roundingModes: 'ROUND_UP'}) 119 | console.log(c, "0.334") 120 | ``` 121 | 122 | ### toDecimal.callMethod 123 | 使用 `toDecimal` 时,调用的 `Decimal` 的方法。使用过 `Decimal` 的小伙伴应该都知道,在通过 `Decimal` 来实现计算的时候,我们往往需要在末尾添调用一个方法来将计算结果转成我们需要的格式。 124 | 默认情况下, `toDecimal` 会调用 `toNumber` 来将计算结果转成一个数字。然而很多时候我们需要的可能不仅仅是一个数字。 125 | :::code-group 126 | ```ts [vite.config.ts] {6} 127 | export default defineConfig({ 128 | plugins: [ 129 | AutoDecimal({ 130 | toDecimal: { 131 | // 这里我们改成 toString 132 | callMethod: 'toString', 133 | } 134 | }) 135 | ] 136 | }) 137 | ``` 138 | ::: 139 | 此时 140 | ```ts {3} 141 | const b = 0.1 + 0.2.toDecimal() 142 | // console.log(b, 0.3) 143 | console.log(b, '0.3') 144 | ``` 145 | 146 | ### toDecimal.precision 147 | 当 `callMethod: 'toFixed'` 时,提供一个保留小数的精度。 148 | :::code-group 149 | ```ts [vite.config.ts] {6} 150 | export default defineConfig({ 151 | plugins: [ 152 | AutoDecimal({ 153 | toDecimal: { 154 | callMethod: 'toFixed', 155 | precision: 3 156 | } 157 | }) 158 | ] 159 | }) 160 | ``` 161 | ::: 162 | 此时 163 | ```ts {3} 164 | const b = 0.1 + 0.2.toDecimal() 165 | // console.log(b, 0.3) 166 | console.log(b, '0.300') 167 | ``` 168 | 169 | ### toDecimal.roundingModes 170 | 当 `callMethod: 'toFixed'` 时,保留小数的舍入模式。[舍入模式详见](https://mikemcl.github.io/decimal.js/#modes) 171 | :::code-group 172 | ```ts [vite.config.ts] {7} 173 | export default defineConfig({ 174 | plugins: [ 175 | AutoDecimal({ 176 | toDecimal: { 177 | callMethod: 'toFixed', 178 | precision: 2, 179 | roundingModes: 'ROUND_UP' 180 | } 181 | }) 182 | ] 183 | }) 184 | ``` 185 | ::: 186 | 此时 187 | ```ts {3} 188 | const b = 0.111 + 0.222.toDecimal() 189 | // console.log(b, 0.33) 190 | console.log(b, '0.34') 191 | ``` 192 | 193 | ### toDecimal.name 194 | 当在配置 `AutoDecimal` 时,更改了 `name` 属性 195 | 196 | :::code-group 197 | ```ts [vite.config.ts] {5} 198 | export default defineConfig({ 199 | plugins: [ 200 | AutoDecimal({ 201 | toDecimal: { 202 | name: '_t' 203 | } 204 | }) 205 | ] 206 | }) 207 | ``` 208 | ::: 209 | ```ts 210 | // 调用方法时,也需要使用更改后的 name 211 | const b = (0.1 + 0.2)._t() 212 | console.log(b, 0.3) 213 | 214 | const c = (0.1111 + 0.2222)._t({precision: 3, callMethod: 'toFixed', roundingModes: 'ROUND_UP'}) 215 | console.log(c, "0.334") 216 | ``` -------------------------------------------------------------------------------- /test/ts.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import fastGlob from 'fast-glob' 4 | import { describe, expect, it } from 'vitest' 5 | import { transform } from '../src/core/unplugin' 6 | 7 | describe('transform', async () => { 8 | const root = resolve(__dirname, 'fixtures') 9 | const files = await fastGlob(['*.ts', '!*function.ts', '!*to-decimal.ts', '!*inject-window.ts'], { 10 | cwd: root, 11 | onlyFiles: true, 12 | }) 13 | for (const file of files) { 14 | const fixture = await fs.readFile(resolve(root, file), 'utf-8') 15 | const transformedCode = transform(fixture, file, { 16 | supportString: true, 17 | tailPatchZero: false, 18 | package: 'decimal.js-light', 19 | toDecimal: false, 20 | dts: false, 21 | decimalName: '__Decimal', 22 | supportNewFunction: false, 23 | })?.code ?? fixture 24 | it(` 25 | ts 26 | input: 27 | const _a = 0.1 + 0.2 28 | output: 29 | const _a = new __Decimal(0.1).plus(0.2).toNumber() 30 | `, () => { 31 | expect(transformedCode).toMatch('const _a = new __Decimal(0.1).plus(0.2).toNumber()') 32 | }) 33 | it(` 34 | ts Class 35 | input: 36 | constructor() { 37 | this.block = 0.1 + 0.2 38 | } 39 | output: 40 | constructor() { 41 | this.block = new __Decimal(0.1).plus(0.2).toNumber() 42 | } 43 | `, () => { 44 | expect(transformedCode).toMatch('this.block = new __Decimal(0.1).plus(0.2).toNumber()') 45 | }) 46 | it(` 47 | ts Class 48 | input: 49 | calc() { 50 | this.block = this.block + 0.7 - 0.9 51 | } 52 | output: 53 | calc() { 54 | this.block = new __Decimal( this.block).plus(0.7).minus(0.9).toNumber() 55 | } 56 | `, () => { 57 | expect(transformedCode).toMatch('this.block = new __Decimal(this.block).plus(0.7).minus(0.9).toNumber()') 58 | }) 59 | it(` 60 | ts Array 61 | input: 62 | const _arr = [0, 0.1 + 0.2, 3] 63 | output: 64 | const _arr = [0, new __Decimal(0.1).plus(0.2).toNumber(), 3] 65 | `, () => { 66 | expect(transformedCode).toMatch('const _arr = [0, new __Decimal(0.1).plus(0.2).toNumber(), 3]') 67 | }) 68 | it(` 69 | ts Object 70 | input: 71 | const _obj_outer = { 72 | transform: 0.1 + 0.2, 73 | } 74 | output: 75 | const _obj_outer = { 76 | transform: new __Decimal(0.1).plus(0.2).toNumber(), 77 | } 78 | `, () => { 79 | expect(transformedCode).toMatch('transform: new __Decimal(0.1).plus(0.2).toNumber(),') 80 | }) 81 | it(` 82 | ts Computation 83 | input: 84 | (0.1 + 0.2) * (1 - 0.9) + (0.5 * 0.6 / (1 - 0.2)) + 0.5 85 | const _computation = (0.1 + 0.2) * (1 - 0.9) + 0.5 * 0.6 / (1 - 0.2) + 0.5 86 | output: 87 | const _computation = new __Decimal(0.1).plus(0.2).times(new __Decimal(1).minus(0.9)).plus(new __Decimal(0.5).times(0.6).div(new __Decimal(1).minus(0.2))).plus(0.5).toNumber() 88 | `, () => { 89 | expect(transformedCode).toMatch('const _computation = new __Decimal(0.1).plus(0.2).times(new __Decimal(1).minus(0.9)).plus(new __Decimal(0.5).times(0.6).div(new __Decimal(1).minus(0.2))).plus(0.5).toNumber()') 90 | }) 91 | it(` 92 | ts splicing 93 | input: 94 | const _splicing = 0.1 + 0.2 + '' 95 | output: 96 | const _splicing = 0.1 + 0.2 + '' 97 | `, () => { 98 | expect(transformedCode).toMatch('const _splicing = 0.1 + 0.2 + \'\'') 99 | }) 100 | it(` 101 | ts next-ad-ignore 102 | input: 103 | // next-ad-ignore 104 | const _s = 0.1 + 0.2 105 | output: 106 | // next-ad-ignore 107 | const _s = 0.1 + 0.2 108 | `, () => { 109 | expect(transformedCode).toMatch('const _s = 0.1 + 0.2') 110 | }) 111 | it(` 112 | ts next-ad-ignore object.property 113 | input: 114 | const _obj_outer = { 115 | // next-ad-ignore 116 | skip: 0.1 + 0.2, 117 | } 118 | output: 119 | const _obj_outer = { 120 | // next-ad-ignore 121 | skip: 0.1 + 0.2, 122 | } 123 | `, () => { 124 | expect(transformedCode).toMatch('skip: 0.1 + 0.2,') 125 | }) 126 | it(` 127 | ts block-ad-ignore 128 | input: 129 | // block-ad-ignore 130 | { 131 | const _obj = 0.1 + 0.2 132 | const _obj_block = 0.1 + 0.2 133 | } 134 | output: 135 | // block-ad-ignore 136 | { 137 | const _obj = 0.1 + 0.2 138 | const _obj_block = 0.1 + 0.2 139 | } 140 | `, () => { 141 | expect(transformedCode).toMatch('const _obj = 0.1 + 0.2') 142 | expect(transformedCode).toMatch('const _obj_block = 0.1 + 0.2') 143 | }) 144 | it(` 145 | ts block-ad-ignore function 146 | input: 147 | // block-ad-ignore 148 | function _test() { 149 | const _block = 0.1 + 0.2 150 | const _ad = 0.1 + 0.2 151 | } 152 | output: 153 | // block-ad-ignore 154 | function _test() { 155 | const _block = 0.1 + 0.2 156 | const _ad = 0.1 + 0.2 157 | } 158 | `, () => { 159 | expect(transformedCode).toMatch('const _block = 0.1 + 0.2') 160 | expect(transformedCode).toMatch('const _ad = 0.1 + 0.2') 161 | }) 162 | 163 | it(` 164 | ts skip integer 165 | input: 166 | const integer = 1 + 2 + 3 167 | output: 168 | const integer = 1 + 2 + 3 169 | `, () => { 170 | expect(transformedCode).toMatch('const integer = 1 + 2 + 3') 171 | }) 172 | it(` 173 | ts skip integer mix 174 | input: 175 | const _mix = integer * (3 + 4) - (5 - 6 + 0.4) 176 | output: 177 | const _mix = new __Decimal(integer).times(3 + 4).minus(new __Decimal(5 - 6).plus(0.4)) 178 | `, () => { 179 | expect(transformedCode).toMatch('const _mix = new __Decimal(integer).times(3 + 4).minus(new __Decimal(5 - 6).plus(0.4))') 180 | }) 181 | } 182 | }) 183 | -------------------------------------------------------------------------------- /src/core/traverse/binary-expression.ts: -------------------------------------------------------------------------------- 1 | import type { Node, NodePath } from '@babel/traverse' 2 | import type { BinaryExpression, StringLiteral } from '@babel/types' 3 | import type { MagicStringAST } from 'magic-string-ast' 4 | import type { Extra, NewFunctionOptions, Operator, Options } from '../../types' 5 | import { isNumericLiteral } from '@babel/types' 6 | import { BASE_COMMENT, LITERALS, OPERATOR, OPERATOR_KEYS } from '../constant' 7 | import { getTransformed } from '../transform' 8 | import { getPkgName, isIntegerValue } from '../utils' 9 | import { getComments } from './comment' 10 | 11 | export function resolveBinaryExpression(path: NodePath, options: Options) { 12 | const extra = (path.node.extra ?? {}) as unknown as Extra 13 | const runtimeOptions = {} as Options 14 | if (options.autoDecimalOptions.toDecimal && !extra.__shouldTransform) 15 | return 16 | if (extra.__shouldTransform) { 17 | path.node.extra = extra.__extra 18 | processBinary(Object.assign(runtimeOptions, extra.options), path) 19 | Object.assign(options, { needImport: runtimeOptions.needImport }) 20 | return 21 | } 22 | processBinary(Object.assign(runtimeOptions, options, { initial: true }), path) 23 | Object.assign(options, { needImport: runtimeOptions.needImport }) 24 | } 25 | export function processBinary(options: Options, path: NodePath) { 26 | const { node } = path 27 | const { left, operator, right } = node 28 | if (!OPERATOR_KEYS.includes(operator)) 29 | return 30 | if (options.integer) { 31 | return 32 | } 33 | if (!options.autoDecimalOptions.toDecimal) { 34 | if (shouldIgnoreComments(path)) { 35 | path.skip() 36 | return 37 | } 38 | if (isStringSplicing(node, options) || mustTailPatchZero(node, options)) { 39 | options.shouldSkip = true 40 | path.skip() 41 | return 42 | } 43 | } 44 | // 如果都是整数则跳过 45 | if (isIntegerValue(left, path, options) && isIntegerValue(right, path, options)) { 46 | options.integer = true 47 | return 48 | } 49 | // 两边都是数字时, 直接转换成 Decimal 50 | if (isNumericLiteral(left) && isNumericLiteral(right)) { 51 | const decimalParts: Array = [`new ${getPkgName(options)}(${left.value})`] 52 | decimalParts.push(`.${OPERATOR[operator as Operator]}(${right.value})`) 53 | if (options.initial && options.callMethod !== 'decimal') { 54 | decimalParts.push(`.${options.callMethod}${options.callArgs}`) 55 | } 56 | options.msa.overwriteNode(node, decimalParts.join('')) 57 | resolveNeedImport(options) 58 | path.skip() 59 | return 60 | } 61 | try { 62 | options.ownerPath ??= path 63 | const leftNode = extractNodeValue(left, options) 64 | const rightNode = extractNodeValue(right, options) 65 | const leftIsInteger = leftNode.integer || isIntegerValue(left, path, options) 66 | const rightIsInteger = rightNode.integer || isIntegerValue(right, path, options) 67 | if (leftIsInteger && rightIsInteger) { 68 | return 69 | } 70 | if (leftNode.shouldSkip || rightNode.shouldSkip) 71 | return 72 | const content = createDecimalOperation(leftNode.msa, rightNode.msa, operator as Operator, options) 73 | options.msa.overwriteNode(node, content) 74 | resolveNeedImport(options) 75 | path.skip() 76 | } 77 | catch (error) { 78 | handleBinaryError(error) 79 | } 80 | } 81 | 82 | function mustTailPatchZero(node: BinaryExpression, options: Options) { 83 | const { left, operator, right } = node 84 | if (operator !== '+') 85 | return false 86 | if (isNumericLiteral(left) && isNumericLiteral(right)) 87 | return false 88 | if (!options.autoDecimalOptions.tailPatchZero) 89 | return false 90 | if (options.initial && (!isNumericLiteral(right) || right.value !== 0)) 91 | return true 92 | } 93 | function isStringSplicing(node: BinaryExpression, options: Options) { 94 | const { left, operator, right } = node 95 | if (operator !== '+') 96 | return false 97 | if (isNumericLiteral(left) && isNumericLiteral(right)) 98 | return false 99 | return [left, right].some(operand => LITERALS.includes(operand.type) && isNonNumericLiteral(operand, options)) 100 | } 101 | function isNonNumericLiteral(node: Node, options: Options) { 102 | if (!LITERALS.includes(node.type)) 103 | return false 104 | if (node.type === 'NullLiteral') 105 | return true 106 | const { value } = node as StringLiteral 107 | const { supportString } = options.autoDecimalOptions 108 | const isString = supportString ? Number.isNaN(Number(value)) : ['StringLiteral', 'TemplateLiteral'].includes(node.type) 109 | return node.type === 'BooleanLiteral' || isString || value.trim() === '' 110 | } 111 | function shouldIgnoreComments(path: NodePath): boolean { 112 | const comments = getComments(path) 113 | return comments?.some(comment => comment.value.includes(BASE_COMMENT)) 114 | } 115 | function createDecimalOperation(leftAst: MagicStringAST, rightAst: MagicStringAST, operator: Operator, options: Options): string { 116 | let leftContent = `new ${getPkgName(options)}(${leftAst.toString()})` 117 | if (leftAst.hasChanged()) { 118 | leftContent = `${leftAst.toString()}` 119 | } 120 | const generateContent = `${leftContent}.${OPERATOR[operator]}(${rightAst.toString()})` 121 | if (options.initial && options.callMethod !== 'decimal') { 122 | return `${generateContent}.${options.callMethod}${options.callArgs}` 123 | } 124 | return generateContent 125 | } 126 | function extractNodeValue(node: Node, options: Options) { 127 | const codeSnippet = options.msa.snipNode(node).toString() 128 | return getTransformed( 129 | codeSnippet, 130 | transOptions => ({ 131 | BinaryExpression: path => processBinary(Object.assign(transOptions, { 132 | decimalPkgName: options.decimalPkgName, 133 | integer: options.integer, 134 | fromNewFunction: options.fromNewFunction, 135 | needImport: options.needImport, 136 | ownerPath: options.ownerPath, 137 | }), path), 138 | }), 139 | options.autoDecimalOptions, 140 | ) 141 | } 142 | function handleBinaryError(error: unknown): never { 143 | if (error instanceof Error) { 144 | throw new SyntaxError(`AutoDecimal compile error: ${error.message}`) 145 | } 146 | throw error 147 | } 148 | function resolveNeedImport(options: Options) { 149 | const supportNewFunction = options.autoDecimalOptions.supportNewFunction as NewFunctionOptions 150 | if (!options.fromNewFunction || (options.fromNewFunction && !supportNewFunction.injectWindow)) { 151 | options.needImport = true 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unplugin-auto-decimal", 3 | "type": "module", 4 | "version": "1.4.7", 5 | "packageManager": "pnpm@9.9.0", 6 | "description": "", 7 | "license": "MIT", 8 | "homepage": "https://lyumg.github.io/unplugin-auto-decimal/", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/lyumg/unplugin-auto-decimal.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/lyumg/unplugin-auto-decimal/issues" 15 | }, 16 | "keywords": [ 17 | "unplugin", 18 | "vite", 19 | "webpack", 20 | "rollup", 21 | "transform", 22 | "auto", 23 | "decimal", 24 | "decimal.js", 25 | "decimal.js-light", 26 | "big.js" 27 | ], 28 | "exports": { 29 | ".": { 30 | "import": { 31 | "types": "./dist/index.d.ts", 32 | "default": "./dist/index.js" 33 | }, 34 | "require": { 35 | "types": "./dist/index.d.cts", 36 | "default": "./dist/index.cjs" 37 | } 38 | }, 39 | "./astro": { 40 | "import": { 41 | "types": "./dist/astro.d.ts", 42 | "default": "./dist/astro.js" 43 | }, 44 | "require": { 45 | "types": "./dist/astro.d.cts", 46 | "default": "./dist/astro.cjs" 47 | } 48 | }, 49 | "./rspack": { 50 | "import": { 51 | "types": "./dist/rspack.d.ts", 52 | "default": "./dist/rspack.js" 53 | }, 54 | "require": { 55 | "types": "./dist/rspack.d.cts", 56 | "default": "./dist/rspack.cjs" 57 | } 58 | }, 59 | "./vite": { 60 | "import": { 61 | "types": "./dist/vite.d.ts", 62 | "default": "./dist/vite.js" 63 | }, 64 | "require": { 65 | "types": "./dist/vite.d.cts", 66 | "default": "./dist/vite.cjs" 67 | } 68 | }, 69 | "./webpack": { 70 | "import": { 71 | "types": "./dist/webpack.d.ts", 72 | "default": "./dist/webpack.js" 73 | }, 74 | "require": { 75 | "types": "./dist/webpack.d.cts", 76 | "default": "./dist/webpack.cjs" 77 | } 78 | }, 79 | "./rollup": { 80 | "import": { 81 | "types": "./dist/rollup.d.ts", 82 | "default": "./dist/rollup.js" 83 | }, 84 | "require": { 85 | "types": "./dist/rollup.d.cts", 86 | "default": "./dist/rollup.cjs" 87 | } 88 | }, 89 | "./esbuild": { 90 | "import": { 91 | "types": "./dist/esbuild.d.ts", 92 | "default": "./dist/esbuild.js" 93 | }, 94 | "require": { 95 | "types": "./dist/esbuild.d.cts", 96 | "default": "./dist/esbuild.cjs" 97 | } 98 | }, 99 | "./nuxt": { 100 | "import": { 101 | "types": "./dist/nuxt.d.ts", 102 | "default": "./dist/nuxt.js" 103 | }, 104 | "require": { 105 | "types": "./dist/nuxt.d.cts", 106 | "default": "./dist/nuxt.cjs" 107 | } 108 | }, 109 | "./farm": { 110 | "import": { 111 | "types": "./dist/farm.d.ts", 112 | "default": "./dist/farm.js" 113 | }, 114 | "require": { 115 | "types": "./dist/farm.d.cts", 116 | "default": "./dist/farm.cjs" 117 | } 118 | }, 119 | "./types": { 120 | "import": { 121 | "types": "./dist/types.d.ts", 122 | "default": "./dist/types.js" 123 | }, 124 | "require": { 125 | "types": "./dist/types.d.cts", 126 | "default": "./dist/types.cjs" 127 | } 128 | }, 129 | "./*": "./*" 130 | }, 131 | "main": "dist/index.cjs", 132 | "module": "dist/index.js", 133 | "types": "dist/index.d.ts", 134 | "typesVersions": { 135 | "*": { 136 | "*": [ 137 | "./dist/*", 138 | "./*" 139 | ] 140 | } 141 | }, 142 | "files": [ 143 | "*.d.ts", 144 | "dist" 145 | ], 146 | "scripts": { 147 | "build": "tsup src/*.ts --format cjs,esm --dts --splitting --clean", 148 | "dev": "tsup src/*.ts --watch src/core", 149 | "docs:dev": "vitepress dev docs", 150 | "docs:build": "vitepress build docs", 151 | "docs:preview": "vitepress preview docs", 152 | "example:vite-vue3": "npm -C examples/vite-vue3 run dev", 153 | "example:vite-vue2": "npm -C examples/vite-vue2 run dev", 154 | "example:vite-react": "npm -C examples/vite-react run dev", 155 | "example:rspack-vue3": "npm -C examples/rspack-vue3 run dev", 156 | "example:vue-cli-vue3": "npm -C examples/vue-cli-vue3 run dev", 157 | "example:vue-cli-vue2": "npm -C examples/vue-cli-vue2 run dev", 158 | "lint": "eslint .", 159 | "typecheck": "tsc", 160 | "play": "npm -C playground run dev", 161 | "release": "bumpp", 162 | "start": "esno src/index.ts", 163 | "test": "vitest", 164 | "prepublishOnly": "node scripts/switch-readme.cjs prepare", 165 | "postpublish": "node scripts/switch-readme.cjs restore" 166 | }, 167 | "peerDependencies": { 168 | "@farmfe/core": ">=1", 169 | "@nuxt/kit": "^3", 170 | "@nuxt/schema": "^3", 171 | "esbuild": "*", 172 | "rollup": "^3", 173 | "vite": ">=3", 174 | "webpack": "^4 || ^5" 175 | }, 176 | "peerDependenciesMeta": { 177 | "@farmfe/core": { 178 | "optional": true 179 | }, 180 | "@nuxt/kit": { 181 | "optional": true 182 | }, 183 | "@nuxt/schema": { 184 | "optional": true 185 | }, 186 | "esbuild": { 187 | "optional": true 188 | }, 189 | "rollup": { 190 | "optional": true 191 | }, 192 | "vite": { 193 | "optional": true 194 | }, 195 | "webpack": { 196 | "optional": true 197 | } 198 | }, 199 | "dependencies": { 200 | "@babel/parser": "7.28.3", 201 | "@babel/traverse": "7.28.3", 202 | "@babel/types": "7.28.2", 203 | "@rollup/pluginutils": "5.2.0", 204 | "@vue/compiler-core": "3.5.18", 205 | "@vue/compiler-sfc": "3.5.18", 206 | "local-pkg": "^1.1.1", 207 | "magic-string-ast": "^1.0.2", 208 | "unplugin": "^2.3.6" 209 | }, 210 | "devDependencies": { 211 | "@antfu/eslint-config": "^5.2.1", 212 | "@nuxt/kit": "^4.0.3", 213 | "@nuxt/schema": "^4.0.3", 214 | "@types/babel__generator": "^7.27.0", 215 | "@types/babel__traverse": "^7.28.0", 216 | "@types/node": "^24.3.0", 217 | "bumpp": "^10.2.3", 218 | "chalk": "^5.6.0", 219 | "eslint": "^9.33.0", 220 | "esno": "^4.8.0", 221 | "fast-glob": "^3.3.3", 222 | "nodemon": "^3.1.10", 223 | "rollup": "^4.46.3", 224 | "ts-node": "^10.9.2", 225 | "tsup": "^8.5.0", 226 | "typescript": "^5.9.2", 227 | "vite": "^7.1.3", 228 | "vitepress": "^1.6.4", 229 | "vitepress-plugin-group-icons": "^1.6.3", 230 | "vitest": "^3.2.4", 231 | "webpack": "^5.101.3" 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Binding, Node, NodePath } from '@babel/traverse' 2 | import type { Identifier, MemberExpression } from '@babel/types' 3 | import type { BigRoundingMode, DecimalLightRoundingMode, DecimalRoundingMode, Options, Package, RoundingModes } from '../types' 4 | import { isArrowFunctionExpression, isBinaryExpression, isCallExpression, isFunctionDeclaration, isFunctionExpression, isIdentifier, isImportDefaultSpecifier, isImportNamespaceSpecifier, isImportSpecifier, isLiteral, isMemberExpression, isNumericLiteral, isStringLiteral, isTemplateLiteral, isVariableDeclarator } from '@babel/types' 5 | import { BIG_RM, DECIMAL_RM, DECIMAL_RM_LIGHT } from './constant' 6 | 7 | export function getRoundingMode(mode: RoundingModes | number, packageName: Package) { 8 | if (typeof mode === 'number') { 9 | return mode 10 | } 11 | if (packageName === 'big.js') { 12 | return BIG_RM[mode as BigRoundingMode] 13 | } 14 | if (packageName === 'decimal.js') { 15 | return DECIMAL_RM[mode as DecimalRoundingMode] 16 | } 17 | return DECIMAL_RM_LIGHT[mode as DecimalLightRoundingMode] 18 | } 19 | export function getRootBinaryExprPath(path: NodePath) { 20 | let parentPath = path.parentPath 21 | let binaryPath = path 22 | let loop = true 23 | while (loop && parentPath) { 24 | if (isBinaryExpression(parentPath.node)) { 25 | binaryPath = parentPath 26 | parentPath = parentPath.parentPath 27 | } 28 | else { 29 | loop = false 30 | } 31 | } 32 | return binaryPath 33 | } 34 | export function getScopeBinding(path: NodePath | null, name?: string | MemberExpression) { 35 | if (!path || !name) 36 | return 37 | if (typeof name !== 'string') { 38 | name = getObjectIdentifierName(name) 39 | } 40 | const binding = path.scope.hasBinding(name) 41 | if (!binding) { 42 | if (!path.scope.path.parentPath) { 43 | return path.scope.getBinding(name) 44 | } 45 | return getScopeBinding(path.scope.path.parentPath, name) 46 | } 47 | return path.scope.getBinding(name)! 48 | } 49 | export function getTargetPath(path: NodePath, isTargetFunction: ((node?: Node | null) => boolean)): NodePath | null { 50 | let loop = true 51 | let parentPath: NodePath | null = path 52 | while (loop && parentPath) { 53 | if (isTargetFunction(parentPath?.parent)) { 54 | loop = false 55 | } 56 | else { 57 | parentPath = parentPath.parentPath 58 | } 59 | } 60 | return parentPath?.parentPath as NodePath | null 61 | } 62 | export function isIntegerValue(node: Node, path: NodePath, options: Options) { 63 | if (options.autoDecimalOptions.toDecimal) { 64 | return false 65 | } 66 | return isNumeric(node, path, options, true) 67 | } 68 | export function isNumberValue(node: Node, path: NodePath, options: Options) { 69 | return isNumeric(node, path, options, false) 70 | } 71 | export function isStringNode(node?: Node | null) { 72 | return isStringLiteral(node) || isTemplateLiteral(node) 73 | } 74 | export function isFunctionNode(node?: Node | null) { 75 | return isArrowFunctionExpression(node) || isFunctionExpression(node) || isFunctionDeclaration(node) 76 | } 77 | export function isImportNode(node?: Node | null) { 78 | return isImportNamespaceSpecifier(node) || isImportDefaultSpecifier(node) || isImportSpecifier(node) 79 | } 80 | export function getFunctionName(path: NodePath) { 81 | if (isFunctionDeclaration(path.node)) { 82 | return path.node.id?.name 83 | } 84 | if (isArrowFunctionExpression(path.node) || isFunctionExpression(path.node)) { 85 | const node = path.parent 86 | if (isVariableDeclarator(node)) { 87 | return (node.id as Identifier).name 88 | } 89 | } 90 | } 91 | export function getPkgName(options: Options) { 92 | if (options.fromNewFunction) { 93 | const { supportNewFunction } = options.autoDecimalOptions 94 | if (typeof supportNewFunction !== 'boolean' && supportNewFunction.injectWindow) { 95 | return `window.${supportNewFunction.injectWindow}` 96 | } 97 | } 98 | return options.decimalPkgName 99 | } 100 | 101 | export function getNodeValue(node: Node, path: NodePath, options: Options, isInteger?: boolean) { 102 | // TIPS 跳过导入的变量和函数调用 103 | if (isFunctionNode(node) || isImportNode(node) || isCallExpression(node)) { 104 | return 105 | } 106 | if (isLiteral(node)) { 107 | return getLiteralValue(node, path, options) 108 | } 109 | const ownerPath = options.ownerPath ?? path 110 | let parentPath: NodePath | null = ownerPath 111 | let binding: Binding | undefined 112 | const name = isIdentifier(node) ? node.name : isMemberExpression(node) ? getObjectIdentifierName(node) : '' 113 | while (!binding && parentPath) { 114 | binding = getScopeBinding(parentPath, name) 115 | parentPath = parentPath.parentPath 116 | } 117 | if (!binding) { 118 | return 119 | } 120 | if (!isVariableDeclarator(binding.path.node)) { 121 | return 122 | } 123 | const { init } = binding.path.node 124 | if (isCallExpression(init)) { 125 | return 126 | } 127 | if (isLiteral(init)) { 128 | return getLiteralValue(init, binding.path, options) 129 | } 130 | if (isIdentifier(init)) { 131 | return getNodeValue(init, binding.path, options, isInteger) 132 | } 133 | } 134 | function isNumeric(node: Node, path: NodePath, options: Options, isInteger = false): boolean { 135 | const value = getNodeValue(node, path, options, isInteger) 136 | if (typeof value === 'undefined') { 137 | return false 138 | } 139 | const isNotNumber = Number.isNaN(Number(value)) 140 | if (isNotNumber) { 141 | return false 142 | } 143 | if (isInteger && options.autoDecimalOptions.supportString) { 144 | return false 145 | } 146 | return isInteger ? !value.toString().includes('.') : true 147 | } 148 | function getObjectIdentifierName(node: MemberExpression) { 149 | if (isMemberExpression(node.object)) { 150 | return getObjectIdentifierName(node.object) 151 | } 152 | if (isIdentifier(node.object)) { 153 | return node.object.name 154 | } 155 | return '' 156 | } 157 | function getLiteralValue(node: Node, path: NodePath, options: Options, isInteger?: boolean) { 158 | if (isNumericLiteral(node)) { 159 | return node.value 160 | } 161 | if (!options.autoDecimalOptions.supportString) { 162 | return 163 | } 164 | if (isStringLiteral(node)) { 165 | return node.value 166 | } 167 | if (isTemplateLiteral(node)) { 168 | const { quasis, expressions } = node 169 | if (!expressions.length) { 170 | return quasis.map(item => item.value.raw).join('') 171 | } 172 | const quasisCopy = quasis.slice(1, -1) 173 | let index = 0 174 | const exprList: any[] = [] 175 | expressions.forEach((expr) => { 176 | if (quasisCopy.length) { 177 | const quasisItem = quasisCopy[index] 178 | const start = quasisItem.loc!.start 179 | const exprStart = expr.loc!.start 180 | if (start.line < exprStart.line || (start.line === exprStart.line && start.column <= exprStart.column)) { 181 | exprList.push(quasisItem.value.raw) 182 | index++ 183 | } 184 | } 185 | exprList.push(getNodeValue(expr, path, options, isInteger)) 186 | }) 187 | if (index < quasisCopy.length - 1) { 188 | const remainingQuasis = quasisCopy.slice(index) 189 | remainingQuasis.forEach(item => exprList.push(item.value.raw)) 190 | } 191 | exprList.unshift(quasis[0].value.raw) 192 | exprList.push(quasis[quasis.length - 1].value.raw) 193 | return exprList.join('') 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 资源 71 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 资源 71 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 资源 71 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 资源 71 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/core/transform.ts: -------------------------------------------------------------------------------- 1 | import type { TraverseOptions } from '@babel/traverse' 2 | import type { 3 | CommentNode, 4 | CompoundExpressionNode, 5 | DirectiveNode, 6 | ElementNode, 7 | ForNode, 8 | IfBranchNode, 9 | InterpolationNode, 10 | TemplateChildNode, 11 | } from '@vue/compiler-core' 12 | import type { SFCScriptBlock } from '@vue/compiler-sfc' 13 | import type { CommentState, InnerAutoDecimalOptions, Options } from '../types' 14 | import { parse } from '@babel/parser' 15 | import traverse from '@babel/traverse' 16 | import { isObjectExpression } from '@babel/types' 17 | import { NodeTypes } from '@vue/compiler-core' 18 | import { parse as vueParse } from '@vue/compiler-sfc' 19 | import { MagicStringAST } from 'magic-string-ast' 20 | import { BLOCK_COMMENT, DECIMAL_PKG_NAME, NEXT_COMMENT, OPERATOR_KEYS, PATCH_DECLARATION, PKG_NAME } from './constant' 21 | // import { resolveOptions } from './options' 22 | import { resolveImportDeclaration, traverseAst } from './traverse' 23 | 24 | export function transformAutoDecimal(code: string, autoDecimalOptions: InnerAutoDecimalOptions) { 25 | const { msa } = getTransformed(code, traverseAst, autoDecimalOptions) 26 | return msa 27 | } 28 | export function transformVueAutoDecimal(code: string, autoDecimalOptions: InnerAutoDecimalOptions) { 29 | const sfcAst = vueParse(code) 30 | const { descriptor } = sfcAst 31 | const { script, scriptSetup, template } = descriptor 32 | const msa = new MagicStringAST(code) 33 | 34 | const getDecimalPkgName = (scriptSection: SFCScriptBlock | null) => { 35 | if (!scriptSection) 36 | return autoDecimalOptions.decimalName || DECIMAL_PKG_NAME 37 | const { decimalPkgName } = getTransformed( 38 | scriptSection.content, 39 | options => ({ 40 | ImportDeclaration: path => resolveImportDeclaration(path, options), 41 | }), 42 | autoDecimalOptions, 43 | ) 44 | return decimalPkgName 45 | } 46 | let decimalPkgName = getDecimalPkgName(scriptSetup) 47 | if (!decimalPkgName) { 48 | decimalPkgName = getDecimalPkgName(script) 49 | } 50 | 51 | function parserTemplate(children: TemplateChildNode[]) { 52 | const commentState: CommentState = { line: 0, block: false, next: false } 53 | children.forEach((child) => { 54 | if (child.type === NodeTypes.TEXT) 55 | return 56 | if (child.type === NodeTypes.COMMENT) { 57 | updateCommentState(child, commentState) 58 | return 59 | } 60 | if (shouldSkipComment(child, commentState, 'block')) 61 | return 62 | 63 | switch (child.type) { 64 | case NodeTypes.INTERPOLATION: 65 | handleInterpolation(child, commentState) 66 | break 67 | case NodeTypes.ELEMENT: 68 | handleElementProps(child, commentState) 69 | break 70 | default: 71 | break 72 | } 73 | if (hasChildrenNode(child) && child.children) { 74 | parserTemplate(child.children as TemplateChildNode[]) 75 | } 76 | }) 77 | } 78 | function hasChildrenNode( 79 | child: TemplateChildNode, 80 | ): child is ElementNode | CompoundExpressionNode | IfBranchNode | ForNode { 81 | const nodeTypes = [NodeTypes.ELEMENT, NodeTypes.COMPOUND_EXPRESSION, NodeTypes.IF_BRANCH, NodeTypes.FOR] 82 | return nodeTypes.includes(child.type) 83 | } 84 | function updateCommentState(commentNode: CommentNode, commentState: CommentState) { 85 | commentState.line = commentNode.loc.start.line 86 | commentState.block = commentNode.content.includes(BLOCK_COMMENT) 87 | commentState.next = commentNode.content.includes(NEXT_COMMENT) 88 | } 89 | function handleInterpolation(interpolationNode: InterpolationNode, commentState: CommentState) { 90 | if (shouldSkipComment(interpolationNode, commentState)) 91 | return 92 | if (interpolationNode.content.type === NodeTypes.COMPOUND_EXPRESSION) 93 | return 94 | 95 | const expContent = interpolationNode.content.content 96 | if (!expContent || !existTargetOperator(expContent)) 97 | return 98 | 99 | const { msa: transformedMsa } = getTransformed( 100 | expContent, 101 | options => traverseAst({ ...options, decimalPkgName }, false), 102 | autoDecimalOptions, 103 | ) 104 | 105 | msa.update(interpolationNode.content.loc.start.offset, interpolationNode.content.loc.end.offset, transformedMsa.toString()) 106 | } 107 | function handleElementProps(elementNode: ElementNode, commentState: CommentState) { 108 | if (shouldSkipComment(elementNode, commentState)) 109 | return 110 | if (!elementNode.props.length) 111 | return 112 | 113 | elementNode.props.forEach((prop) => { 114 | if (prop.type === NodeTypes.ATTRIBUTE) 115 | return 116 | if (!prop.exp || prop.exp.type === NodeTypes.COMPOUND_EXPRESSION) 117 | return 118 | 119 | const { loc } = prop.exp 120 | let isObjExpr = false 121 | let content = prop.exp.content 122 | if (!content || !existTargetOperator(content)) 123 | return 124 | if (isBuiltInDirective(prop)) 125 | return 126 | if (prop.exp.ast && isObjectExpression(prop.exp.ast)) { 127 | isObjExpr = true 128 | content = `${PATCH_DECLARATION}${content}` 129 | } 130 | const { msa: transformedMsa } = getTransformed( 131 | content, 132 | options => traverseAst({ ...options, decimalPkgName }, false), 133 | autoDecimalOptions, 134 | ) 135 | if (isObjExpr) { 136 | transformedMsa.remove(0, PATCH_DECLARATION.length) 137 | } 138 | msa.update(loc.start.offset, loc.end.offset, transformedMsa.toString()) 139 | }) 140 | } 141 | 142 | function isBuiltInDirective(prop: DirectiveNode) { 143 | return ['for', 'html', 'text'].includes(prop.name) 144 | } 145 | 146 | function existTargetOperator(content: string) { 147 | return OPERATOR_KEYS.some(key => content.includes(key)) 148 | } 149 | function shouldSkipComment(child: TemplateChildNode, comment: CommentState, property: 'next' | 'block' = 'next') { 150 | return comment[property] && comment.line + 1 === child.loc.start.line 151 | } 152 | if (template) { 153 | const { ast, attrs = {} } = template 154 | if (!attrs['ad-ignore'] && ast?.children) { 155 | parserTemplate(ast.children) 156 | } 157 | } 158 | let needsImport = msa.hasChanged() 159 | const parseScript = (scriptSection: SFCScriptBlock | null) => { 160 | if (!scriptSection) 161 | return 162 | const { start, end } = scriptSection.loc 163 | const { msa: transformedMsa, imported } = getTransformed( 164 | scriptSection.content, 165 | options => traverseAst(options, true, needsImport), 166 | autoDecimalOptions, 167 | ) 168 | if (needsImport) { 169 | needsImport = !imported 170 | } 171 | msa.update(start.offset, end.offset, transformedMsa.toString()) 172 | } 173 | parseScript(scriptSetup) 174 | parseScript(script) 175 | if (needsImport) { 176 | msa.append(` 177 | 185 | `) 186 | } 187 | 188 | return msa 189 | } 190 | export function getTransformed( 191 | code: string, 192 | traverseOptions: (options: Options) => TraverseOptions, 193 | autoDecimalOptions: InnerAutoDecimalOptions, 194 | ) { 195 | const ast = parse(code, { 196 | sourceType: 'module', 197 | plugins: ['typescript', 'jsx'], 198 | }) 199 | const msa = new MagicStringAST(code) 200 | const options: Options = { 201 | autoDecimalOptions, 202 | imported: false, 203 | msa, 204 | decimalPkgName: autoDecimalOptions.decimalName || DECIMAL_PKG_NAME, 205 | initial: false, 206 | integer: false, 207 | shouldSkip: false, 208 | callArgs: '()', 209 | callMethod: 'toNumber', 210 | needImport: false, 211 | fromNewFunction: false, 212 | } 213 | // @ts-expect-error adapter cjs/esm 214 | const babelTraverse = traverse.default ?? traverse 215 | babelTraverse(ast, traverseOptions(options)) 216 | return options 217 | } 218 | -------------------------------------------------------------------------------- /src/core/traverse/new-function.ts: -------------------------------------------------------------------------------- 1 | import type { Binding, NodePath } from '@babel/traverse' 2 | import type { ArrowFunctionExpression, AssignmentExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, NewExpression, Node, StringLiteral, TemplateLiteral } from '@babel/types' 3 | import type { NewFunctionOptions, Options } from '../../types' 4 | import { isArrayExpression, isAssignmentExpression, isCallExpression, isIdentifier, isMemberExpression, isNodesEquivalent, isNumericLiteral, isObjectProperty, isReturnStatement, isStatement, isStringLiteral, isVariableDeclarator } from '@babel/types' 5 | import { traverseAst } from '.' 6 | import { RETURN_DECLARATION_CODE, RETURN_DECLARATION_FN, RETURN_DECLARATION_PREFIX } from '../constant' 7 | import { getTransformed } from '../transform' 8 | import { getFunctionName, getScopeBinding, getTargetPath, isFunctionNode, isStringNode } from '../utils' 9 | 10 | // TIPS 使用 new Function 时,需要将 __Decimal 以参数的形式传递过去 11 | export function resolveNewFunctionExpression(path: NodePath, options: Options) { 12 | if (!options.autoDecimalOptions.supportNewFunction) 13 | return 14 | const { node } = path 15 | const { callee, arguments: args } = node 16 | if (!isIdentifier(callee) || callee.name !== 'Function') 17 | return 18 | if (args.length === 0) 19 | return 20 | const lastArg = args[args.length - 1] 21 | resolveReturnParam(path, lastArg, options) 22 | const { injectWindow } = options.autoDecimalOptions.supportNewFunction as NewFunctionOptions 23 | if (!injectWindow) { 24 | provideDecimal(path, lastArg as Expression, options) 25 | } 26 | } 27 | 28 | /** 29 | * 处理 new Function return 参数 30 | * 目前仅支持字符串形式、变量、函数调用的方式传递 return 参数 31 | * 1. new Function('a', 'b', 'return a + b') 32 | * 2. const assignment = 'a + b'; 33 | * new Function('a', 'b', assignment) 34 | * 3. const arrowFunc = () => 'a + b'; 35 | * const assignmentFunc = function() { 36 | * something ............ 37 | * return 'a + b' 38 | * } 39 | * function func() { 40 | * something ............ 41 | * return 'a + b' 42 | * } 43 | * new Function('a', 'b', arrowFunc / assignmentFunc / func) 44 | */ 45 | function resolveReturnParam(path: NodePath, node: Node, options: Options) { 46 | if (isStringNode(node)) { 47 | return resolveStringTemplateLiteral(node, options) 48 | } 49 | if (isIdentifier(node) || (isCallExpression(node) && isIdentifier(node.callee))) { 50 | const name = isIdentifier(node) ? node.name : (node.callee as Identifier).name 51 | const binding = getScopeBinding(path, name) 52 | resolveVariableParam(options, binding, name) 53 | } 54 | } 55 | 56 | function resolveVariableParam(options: Options, binding?: Binding, name?: string) { 57 | if (!binding) 58 | return 59 | if (binding.kind === 'param') { 60 | resolveVariableOfParam(binding, options, name) 61 | } 62 | const { constantViolations, path } = binding 63 | if (isVariableDeclarator(path.node)) { 64 | const { init } = path.node 65 | if (!init) 66 | return 67 | resolveAssignmentExpression(path, init, options) 68 | } 69 | constantViolations.forEach((cv) => { 70 | if (isAssignmentExpression(cv.node)) { 71 | const { right } = cv.node 72 | resolveAssignmentExpression(cv, right, options) 73 | } 74 | }) 75 | } 76 | 77 | function resolveAssignmentExpression(path: NodePath, node: Expression, options: Options) { 78 | if (isStringNode(node)) { 79 | return resolveStringTemplateLiteral(node, options) 80 | } 81 | if (isFunctionNode(node)) { 82 | return resolveFunction(node, options) 83 | } 84 | if (isIdentifier(node)) { 85 | const binding = getScopeBinding(path, node.name) 86 | return resolveVariableParam(options, binding) 87 | } 88 | if (isCallExpression(node)) { 89 | const variableName = (node.callee as Identifier).name 90 | const binding = getScopeBinding(path, variableName) 91 | if (!binding) 92 | return 93 | const pathNode = binding.path.node 94 | if (isFunctionNode(pathNode)) { 95 | resolveFunction(pathNode, options) 96 | return 97 | } 98 | if (isVariableDeclarator(pathNode) && isFunctionNode(pathNode.init)) { 99 | resolveFunction(pathNode.init, options) 100 | return 101 | } 102 | console.warn(`未处理的节点,line: ${node.loc!.start.line}, ${node.loc!.end.index}; column: ${node.loc!.start.column}, ${node.loc!.end.column}`) 103 | } 104 | } 105 | // 解析参数形式的变量 106 | function resolveVariableOfParam(binding: Binding, options: Options, name?: string) { 107 | if (!isFunctionNode(binding.scope.block) || !name) { 108 | return 109 | } 110 | const { block, path } = binding.scope 111 | const { params } = block 112 | if (!params.length) 113 | return 114 | const paramsIndex = params.findIndex(param => (param as Identifier).name === name) 115 | if (paramsIndex < 0) 116 | return 117 | const fnName = getFunctionName(path) 118 | if (!fnName || !path.parentPath) 119 | return 120 | const parentBinding = getScopeBinding(path.parentPath, fnName) 121 | if (!parentBinding) 122 | return 123 | parentBinding.referencePaths.forEach((nodePath) => { 124 | if (!isCallExpression(nodePath.parent)) 125 | return 126 | const targetParams = nodePath.parent.arguments[paramsIndex] 127 | if (!targetParams) 128 | return 129 | resolveReturnParam(nodePath, targetParams, options) 130 | }) 131 | } 132 | 133 | function provideDecimal(path: NodePath, node: Expression, options: Options) { 134 | if (!options.msa.hasChanged()) 135 | return 136 | let parentPath: null | NodePath = path.parentPath 137 | let params: string | number 138 | // Decimal 形参 139 | const decimalParamsContent = `'${options.decimalPkgName}', ${options.msa.snipNode(node)}` 140 | const { parent } = path 141 | let callName = '' 142 | if (isCallExpression(parent)) { 143 | options.msa.update(node.start!, node.end!, decimalParamsContent) 144 | options.msa.update(parent.end! - 1, parent.end!, `, ${options.decimalPkgName})`) 145 | return 146 | } 147 | if (isAssignmentExpression(parent)) { 148 | const { left } = parent 149 | if (isIdentifier(left)) { 150 | callName = left.name 151 | } 152 | // 如果为 obj.x.x.x or arr[x][x][x] 形式调用 153 | else if (isMemberExpression(left)) { 154 | const binding = getScopeBinding(parentPath, left) 155 | if (!binding) 156 | return 157 | binding.referencePaths.forEach((reference) => { 158 | const referenceParent = reference.parentPath!.parent 159 | if (isCallExpression(referenceParent)) { 160 | options.msa.update(referenceParent.end! - 1, referenceParent.end!, `, ${options.decimalPkgName})`) 161 | return 162 | } 163 | if (isAssignmentExpression(referenceParent)) { 164 | const { right } = referenceParent 165 | if (isNodesEquivalent(right, path.node)) { 166 | options.msa.update(node.start!, node.end!, decimalParamsContent) 167 | } 168 | return 169 | } 170 | if (isMemberExpression(referenceParent)) { 171 | const targetPath = getTargetPath(reference, isCallExpression) 172 | if (targetPath) { 173 | options.msa.update(targetPath.node.end! - 1, targetPath.node.end!, `, ${options.decimalPkgName})`) 174 | } 175 | else { 176 | const targetPath = getTargetPath(reference, isAssignmentExpression) 177 | if (!targetPath) 178 | return 179 | const { right } = targetPath.node as AssignmentExpression 180 | if (isNodesEquivalent(right, path.node)) { 181 | options.msa.update(node.start!, node.end!, decimalParamsContent) 182 | } 183 | } 184 | } 185 | }) 186 | return 187 | } 188 | } 189 | else if (!isVariableDeclarator(parent)) { 190 | parentPath = getTargetPath(path, isVariableDeclarator) 191 | if (!parentPath) 192 | return 193 | // TODO MemberExpression 目前不支持变量引用 [variable] 形式调用 194 | if (isArrayExpression(parent)) { 195 | params = path.key! 196 | } 197 | else if (isObjectProperty(parent)) { 198 | params = (parent.key as Identifier).name 199 | } 200 | } 201 | if (!callName) { 202 | if (!parentPath || !isVariableDeclarator(parentPath.node)) { 203 | return 204 | } 205 | callName = (parentPath.node.id as Identifier)?.name 206 | if (!callName) 207 | return 208 | } 209 | const binding = getScopeBinding(path, callName) 210 | if (!binding?.referenced) 211 | return 212 | options.msa.update(node.start!, node.end!, decimalParamsContent) 213 | binding.referencePaths.forEach((referencePath) => { 214 | const { parent } = referencePath 215 | if (isCallExpression(parent)) { 216 | options.msa.update(parent.end! - 1, parent.end!, `, ${options.decimalPkgName})`) 217 | return 218 | } 219 | if (isMemberExpression(parent)) { 220 | if (isNumericLiteral(parent.property) || isIdentifier(parent.property)) { 221 | const targetParams = isNumericLiteral(parent.property) ? parent.property.value : parent.property.name 222 | if (targetParams !== params) { 223 | return 224 | } 225 | const targetPath = getTargetPath(referencePath, isCallExpression) 226 | if (!targetPath) 227 | return 228 | options.msa.update(targetPath.node.end! - 1, targetPath.node.end!, `, ${options.decimalPkgName})`) 229 | } 230 | } 231 | }) 232 | } 233 | 234 | function resolveStringTemplateLiteral(node: StringLiteral | TemplateLiteral, options: Options) { 235 | let rawString = '' 236 | let quote = '\'' 237 | if (isStringLiteral(node)) { 238 | rawString = node.value 239 | } 240 | else { 241 | quote = '`' 242 | rawString = options.msa.snipNode(node).toString().slice(1, -1) 243 | } 244 | const { autoDecimalOptions } = options 245 | const supportNewFunction = autoDecimalOptions.supportNewFunction as NewFunctionOptions 246 | const code = RETURN_DECLARATION_FN.replace(RETURN_DECLARATION_CODE, rawString) 247 | const toDecimalParams = supportNewFunction.toDecimal ?? false 248 | const runtimeOptions = {} as Options 249 | const { msa: transformedMsa } = getTransformed(code, opts => traverseAst(Object.assign(runtimeOptions, opts, { 250 | fromNewFunction: true, 251 | needImport: options.needImport, 252 | }), false), { 253 | ...autoDecimalOptions, 254 | toDecimal: toDecimalParams, 255 | }) 256 | if (transformedMsa.hasChanged()) { 257 | Object.assign(options, { 258 | fromNewFunction: runtimeOptions.fromNewFunction, 259 | needImport: runtimeOptions.needImport, 260 | }) 261 | const result = transformedMsa.toString().replace(RETURN_DECLARATION_PREFIX, '').slice(0, -1) 262 | options.msa.overwriteNode(node, `${quote}${result}${quote}`) 263 | } 264 | } 265 | function resolveFunction(node: FunctionDeclaration | ArrowFunctionExpression | FunctionExpression, options: Options) { 266 | const { body } = node 267 | if (isStringNode(body)) { 268 | resolveStringTemplateLiteral(body, options) 269 | return 270 | } 271 | if (isStatement(body)) { 272 | const lastNode = body.body[body.body.length - 1] 273 | if (!isReturnStatement(lastNode)) { 274 | return 275 | } 276 | const { argument } = lastNode 277 | if (!argument || !isStringNode(argument)) { 278 | return 279 | } 280 | resolveStringTemplateLiteral(argument, options) 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /docs/.vitepress/assets/rspack.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------