')
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 |
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 |
81 | ...something
82 |
83 | ```
84 |
85 | `ad-ignore` 只影响在 `template` 中定义的计算表达式是否转换, 不会影响到 `script` 中定义的计算表达式。
86 | ```vue
87 |
88 |
89 |
95 |
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 |
108 |
109 |
110 |
111 |
112 |
113 | {{ 0.1 + 0.2 }}
114 |
115 |
116 |
117 |
118 |
119 |
{{ 0.1 + 0.2 }}
120 |
121 |
122 |
123 |
124 | {{ 0.1 + 0.2 }}
125 |
126 |
127 |
128 | {{ 0.1 + 0.2 }}
129 |
130 |
131 |
132 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------