UNPKG

unplugin-auto-decimal

Version:

<h1 align="center" style="margin-top: -10px">AutoDecimal</h1> <div align="center" style="margin-bottom:10px"> <a href="https://www.npmjs.com/package/unplugin-auto-decimal"> <img src="https://img.shields.io/npm/v/unplugin-auto-decimal"/> </a> </div

1,241 lines (1,219 loc) 41.8 kB
// src/core/unplugin.ts import { createFilter } from "@rollup/pluginutils"; import { isPackageExists as isPackageExists2 } from "local-pkg"; import { createUnplugin } from "unplugin"; // src/core/constant.ts var PREFIX = "ad"; var BASE_COMMENT = `${PREFIX}-ignore`; var NEXT_COMMENT = `next-${BASE_COMMENT}`; var FILE_COMMENT = `file-${BASE_COMMENT}`; var BLOCK_COMMENT = `block-${BASE_COMMENT}`; var PATCH_DECLARATION = "const __PATCH_DECLARATION__ = "; var RETURN_DECLARATION_CODE = "###code###"; var RETURN_FUNCTION_NAME = "__RETURN_DECLARATION_FN__"; var RETURN_DECLARATION_PREFIX = `function ${RETURN_FUNCTION_NAME}() {`; var RETURN_DECLARATION_FN = `${RETURN_DECLARATION_PREFIX}${RETURN_DECLARATION_CODE}}`; var LITERALS = ["StringLiteral", "NullLiteral", "BooleanLiteral", "TemplateLiteral"]; var OPERATOR = { "+": "plus", "-": "minus", "*": "times", "/": "div", "**": "pow" }; var OPERATOR_KEYS = Object.keys(OPERATOR); var REGEX_SUPPORTED_EXT = /\.([cm]?[jt]s)x?$/; var REGEX_VUE = [/\.vue$/, /\.vue\?vue/, /\.vue\?v=/]; var REGEX_NODE_MODULES = /node_modules/; var DECIMAL_PKG_NAME = "__Decimal"; var PKG_NAME = "decimal.js-light"; var DEFAULT_TO_DECIMAL_CONFIG = { precision: 2, p: 2, roundingModes: "ROUND_HALF_UP", rm: "ROUND_HALF_UP", callMethod: "toNumber", cm: "toNumber", name: "toDecimal" }; var DEFAULT_NEW_FUNCTION_CONFIG = { injectWindow: void 0, toDecimal: false }; var DECIMAL_RM_LIGHT = Object.freeze({ ROUND_UP: 0, ROUND_DOWN: 1, ROUND_CEIL: 2, ROUND_FLOOR: 3, ROUND_HALF_UP: 4, ROUND_HALF_DOWN: 5, ROUND_HALF_EVEN: 6, ROUND_HALF_CEIL: 7, ROUND_HALF_FLOOR: 8 }); var DECIMAL_RM = Object.freeze({ ...DECIMAL_RM_LIGHT, EUCLID: 9 }); var BIG_RM = Object.freeze({ ROUND_DOWN: 0, ROUND_HALF_UP: 1, ROUND_HALF_DOWN: 2, ROUND_UP: 3 }); // src/core/generate.ts import { existsSync } from "fs"; import { mkdir, readFile, writeFile } from "fs/promises"; import { dirname } from "path"; async function generateDeclaration(options) { const filePath = options.dts; const toDecimal = typeof options.toDecimal === "boolean" ? DEFAULT_TO_DECIMAL_CONFIG : options.toDecimal; const content = `/* eslint-disable */ // @ts-nocheck // Generated by unplugin-auto-decimal type ToDecimal = import('unplugin-auto-decimal/types').ToDecimal declare interface String { ${toDecimal.name}: ToDecimal } declare interface Number { ${toDecimal.name}: ToDecimal } `; const originalContent = existsSync(filePath) ? await readFile(filePath, "utf-8") : ""; if (originalContent !== content) { await writeDeclaration(filePath, content); } } async function writeDeclaration(filePath, content) { await mkdir(dirname(filePath), { recursive: true }); return await writeFile(filePath, content, "utf-8"); } // src/core/options.ts import { resolve } from "path"; import process from "process"; import { isPackageExists } from "local-pkg"; var rootPath = process.cwd(); var defaultOptions = { supportString: false, tailPatchZero: false, package: "decimal.js-light", toDecimal: false, dts: isPackageExists("typescript"), supportNewFunction: false, decimalName: "__Decimal" }; function resolveOptions(rawOptions) { const options = Object.assign({}, defaultOptions, rawOptions); options.dts = !options.dts ? false : resolve(rootPath, typeof options.dts === "string" ? options.dts : "auto-decimal.d.ts"); options.toDecimal = !options.toDecimal ? false : options.toDecimal === true ? { ...DEFAULT_TO_DECIMAL_CONFIG } : Object.assign({}, DEFAULT_TO_DECIMAL_CONFIG, options.toDecimal); options.supportNewFunction = !options.supportNewFunction ? false : options.supportNewFunction === true ? { toDecimal: options.toDecimal } : { ...DEFAULT_NEW_FUNCTION_CONFIG, toDecimal: options.toDecimal, ...options.supportNewFunction }; return options; } function mergeToDecimalOptions(rawOptions, toDecimalOptions) { if (typeof toDecimalOptions === "boolean") { return rawOptions; } const precision = toDecimalOptions.precision ?? toDecimalOptions.p ?? rawOptions.precision; const callMethod = toDecimalOptions.callMethod ?? toDecimalOptions.cm ?? rawOptions.callMethod; const roundingModes = toDecimalOptions.roundingModes ?? toDecimalOptions.rm ?? rawOptions.roundingModes; return Object.assign(rawOptions, { precision, callMethod, roundingModes, p: precision, cm: callMethod, rm: roundingModes }); } // src/core/transform.ts import { parse } from "@babel/parser"; import traverse from "@babel/traverse"; import { isObjectExpression as isObjectExpression3 } from "@babel/types"; import { NodeTypes } from "@vue/compiler-core"; import { parse as vueParse } from "@vue/compiler-sfc"; import { MagicStringAST } from "magic-string-ast"; // src/core/traverse/ast.ts import { isJSXEmptyExpression as isJSXEmptyExpression2 } from "@babel/types"; // src/core/traverse/binary-expression.ts import { isNumericLiteral as isNumericLiteral2 } from "@babel/types"; // src/core/utils.ts import { isArrowFunctionExpression, isBinaryExpression, isCallExpression, isFunctionDeclaration, isFunctionExpression, isIdentifier, isImportDefaultSpecifier, isImportNamespaceSpecifier, isImportSpecifier, isLiteral, isMemberExpression, isNumericLiteral, isStringLiteral, isTemplateLiteral, isVariableDeclarator } from "@babel/types"; function getRoundingMode(mode, packageName) { if (typeof mode === "number") { return mode; } if (packageName === "big.js") { return BIG_RM[mode]; } if (packageName === "decimal.js") { return DECIMAL_RM[mode]; } return DECIMAL_RM_LIGHT[mode]; } function getRootBinaryExprPath(path) { let parentPath = path.parentPath; let binaryPath = path; let loop = true; while (loop && parentPath) { if (isBinaryExpression(parentPath.node)) { binaryPath = parentPath; parentPath = parentPath.parentPath; } else { loop = false; } } return binaryPath; } function getScopeBinding(path, name) { if (!path || !name) return; if (typeof name !== "string") { name = getObjectIdentifierName(name); } const binding = path.scope.hasBinding(name); if (!binding) { if (!path.scope.path.parentPath) { return path.scope.getBinding(name); } return getScopeBinding(path.scope.path.parentPath, name); } return path.scope.getBinding(name); } function getTargetPath(path, isTargetFunction) { let loop = true; let parentPath = path; while (loop && parentPath) { if (isTargetFunction(parentPath?.parent)) { loop = false; } else { parentPath = parentPath.parentPath; } } return parentPath?.parentPath; } function isIntegerValue(node, path, options) { if (options.autoDecimalOptions.toDecimal) { return false; } return isNumeric(node, path, options, true); } function isStringNode(node) { return isStringLiteral(node) || isTemplateLiteral(node); } function isFunctionNode(node) { return isArrowFunctionExpression(node) || isFunctionExpression(node) || isFunctionDeclaration(node); } function isImportNode(node) { return isImportNamespaceSpecifier(node) || isImportDefaultSpecifier(node) || isImportSpecifier(node); } function getFunctionName(path) { if (isFunctionDeclaration(path.node)) { return path.node.id?.name; } if (isArrowFunctionExpression(path.node) || isFunctionExpression(path.node)) { const node = path.parent; if (isVariableDeclarator(node)) { return node.id.name; } } } function getPkgName(options) { if (options.fromNewFunction) { const { supportNewFunction } = options.autoDecimalOptions; if (typeof supportNewFunction !== "boolean" && supportNewFunction.injectWindow) { return `window.${supportNewFunction.injectWindow}`; } } return options.decimalPkgName; } function getNodeValue(node, path, options, isInteger) { if (isFunctionNode(node) || isImportNode(node) || isCallExpression(node)) { return; } if (isLiteral(node)) { return getLiteralValue(node, path, options); } const ownerPath = options.ownerPath ?? path; let parentPath = ownerPath; let binding; const name = isIdentifier(node) ? node.name : isMemberExpression(node) ? getObjectIdentifierName(node) : ""; while (!binding && parentPath) { binding = getScopeBinding(parentPath, name); parentPath = parentPath.parentPath; } if (!binding) { return; } if (!isVariableDeclarator(binding.path.node)) { return; } const { init } = binding.path.node; if (isCallExpression(init)) { return; } if (isLiteral(init)) { return getLiteralValue(init, binding.path, options); } if (isIdentifier(init)) { return getNodeValue(init, binding.path, options, isInteger); } } function isNumeric(node, path, options, isInteger = false) { const value = getNodeValue(node, path, options, isInteger); if (typeof value === "undefined") { return false; } const isNotNumber = Number.isNaN(Number(value)); if (isNotNumber) { return false; } if (isInteger && options.autoDecimalOptions.supportString) { return false; } return isInteger ? !value.toString().includes(".") : true; } function getObjectIdentifierName(node) { if (isMemberExpression(node.object)) { return getObjectIdentifierName(node.object); } if (isIdentifier(node.object)) { return node.object.name; } return ""; } function getLiteralValue(node, path, options, isInteger) { if (isNumericLiteral(node)) { return node.value; } if (!options.autoDecimalOptions.supportString) { return; } if (isStringLiteral(node)) { return node.value; } if (isTemplateLiteral(node)) { const { quasis, expressions } = node; if (!expressions.length) { return quasis.map((item) => item.value.raw).join(""); } const quasisCopy = quasis.slice(1, -1); let index = 0; const exprList = []; expressions.forEach((expr) => { if (quasisCopy.length) { const quasisItem = quasisCopy[index]; const start = quasisItem.loc.start; const exprStart = expr.loc.start; if (start.line < exprStart.line || start.line === exprStart.line && start.column <= exprStart.column) { exprList.push(quasisItem.value.raw); index++; } } exprList.push(getNodeValue(expr, path, options, isInteger)); }); if (index < quasisCopy.length - 1) { const remainingQuasis = quasisCopy.slice(index); remainingQuasis.forEach((item) => exprList.push(item.value.raw)); } exprList.unshift(quasis[0].value.raw); exprList.push(quasis[quasis.length - 1].value.raw); return exprList.join(""); } } // src/core/traverse/comment.ts import { isJSXElement, isJSXEmptyExpression, isJSXExpressionContainer, isJSXOpeningElement } from "@babel/types"; function getComments(path) { const leadingComments = path.node.leadingComments ?? []; const trailingComments = path.node.trailingComments ?? []; const currentLineComments = trailingComments.filter((comment) => { return comment.loc?.start.line === path.node.loc?.start.line && comment.value.includes(BASE_COMMENT); }); return [...leadingComments, ...currentLineComments]; } function blockComment(path) { skipAutoDecimalComment(path, BLOCK_COMMENT); } function nextComment(path) { skipAutoDecimalComment(path, NEXT_COMMENT); } function innerComment(path, igc = [NEXT_COMMENT, BLOCK_COMMENT]) { skipAutoDecimalComment(path, igc, true); } function skipAutoDecimalComment(path, igc, isJSX = false) { let comments; let startLine = -1; const rawPath = path; if (isJSX) { if (isJSXOpeningElement(path.node)) { path = path.parentPath; startLine = path.node.loc?.start.line ?? -1; } else if (isJSXExpressionContainer(path.node)) { startLine = path.node.expression.loc?.start.line ?? -1; } let prevPath = path.getPrevSibling(); if (!prevPath.node) return; while (!isJSXExpressionContainer(prevPath.node) || !isJSXEmptyExpression(prevPath.node.expression) || startLine !== -1) { if (startLine !== -1 && isJSXExpressionContainer(prevPath.node)) { if (isJSXEmptyExpression(prevPath.node.expression)) break; const exprStartLine = prevPath.node.loc?.start.line ?? 0; if (exprStartLine !== startLine) { startLine = 0; break; } } prevPath = prevPath.getPrevSibling(); if (!prevPath.node || isJSXElement(prevPath.node)) { if (isJSXElement(prevPath.node)) { const jsxElementStartLine = prevPath.node.loc?.start.line ?? 0; if (startLine === jsxElementStartLine) { continue; } } return; } } if (!isJSXExpressionContainer(prevPath.node)) return; const { expression } = prevPath.node; if (isJSXEmptyExpression(expression)) { comments = expression.innerComments ?? []; } } else { comments = getComments(path); } const ignoreComment = Array.isArray(igc) ? igc : [igc]; if (!comments) return; const isIgnore = comments.some((comment) => ignoreComment.some((ig) => comment.value.includes(ig))) ?? false; if (isIgnore) { rawPath.skip(); } } // src/core/traverse/binary-expression.ts function resolveBinaryExpression(path, options) { const extra = path.node.extra ?? {}; const runtimeOptions = {}; if (options.autoDecimalOptions.toDecimal && !extra.__shouldTransform) return; if (extra.__shouldTransform) { path.node.extra = extra.__extra; processBinary(Object.assign(runtimeOptions, extra.options), path); Object.assign(options, { needImport: runtimeOptions.needImport }); return; } processBinary(Object.assign(runtimeOptions, options, { initial: true }), path); Object.assign(options, { needImport: runtimeOptions.needImport }); } function processBinary(options, path) { const { node } = path; const { left, operator, right } = node; if (!OPERATOR_KEYS.includes(operator)) return; if (options.integer) { return; } if (!options.autoDecimalOptions.toDecimal) { if (shouldIgnoreComments(path)) { path.skip(); return; } if (isStringSplicing(node, options) || mustTailPatchZero(node, options)) { options.shouldSkip = true; path.skip(); return; } } if (isIntegerValue(left, path, options) && isIntegerValue(right, path, options)) { options.integer = true; return; } if (isNumericLiteral2(left) && isNumericLiteral2(right)) { const decimalParts = [`new ${getPkgName(options)}(${left.value})`]; decimalParts.push(`.${OPERATOR[operator]}(${right.value})`); if (options.initial && options.callMethod !== "decimal") { decimalParts.push(`.${options.callMethod}${options.callArgs}`); } options.msa.overwriteNode(node, decimalParts.join("")); resolveNeedImport(options); path.skip(); return; } try { options.ownerPath ??= path; const leftNode = extractNodeValue(left, options); const rightNode = extractNodeValue(right, options); const leftIsInteger = leftNode.integer || isIntegerValue(left, path, options); const rightIsInteger = rightNode.integer || isIntegerValue(right, path, options); if (leftIsInteger && rightIsInteger) { return; } if (leftNode.shouldSkip || rightNode.shouldSkip) return; const content = createDecimalOperation(leftNode.msa, rightNode.msa, operator, options); options.msa.overwriteNode(node, content); resolveNeedImport(options); path.skip(); } catch (error) { handleBinaryError(error); } } function mustTailPatchZero(node, options) { const { left, operator, right } = node; if (operator !== "+") return false; if (isNumericLiteral2(left) && isNumericLiteral2(right)) return false; if (!options.autoDecimalOptions.tailPatchZero) return false; if (options.initial && (!isNumericLiteral2(right) || right.value !== 0)) return true; } function isStringSplicing(node, options) { const { left, operator, right } = node; if (operator !== "+") return false; if (isNumericLiteral2(left) && isNumericLiteral2(right)) return false; return [left, right].some((operand) => LITERALS.includes(operand.type) && isNonNumericLiteral(operand, options)); } function isNonNumericLiteral(node, options) { if (!LITERALS.includes(node.type)) return false; if (node.type === "NullLiteral") return true; const { value } = node; const { supportString } = options.autoDecimalOptions; const isString = supportString ? Number.isNaN(Number(value)) : ["StringLiteral", "TemplateLiteral"].includes(node.type); return node.type === "BooleanLiteral" || isString || value.trim() === ""; } function shouldIgnoreComments(path) { const comments = getComments(path); return comments?.some((comment) => comment.value.includes(BASE_COMMENT)); } function createDecimalOperation(leftAst, rightAst, operator, options) { let leftContent = `new ${getPkgName(options)}(${leftAst.toString()})`; if (leftAst.hasChanged()) { leftContent = `${leftAst.toString()}`; } const generateContent = `${leftContent}.${OPERATOR[operator]}(${rightAst.toString()})`; if (options.initial && options.callMethod !== "decimal") { return `${generateContent}.${options.callMethod}${options.callArgs}`; } return generateContent; } function extractNodeValue(node, options) { const codeSnippet = options.msa.snipNode(node).toString(); return getTransformed( codeSnippet, (transOptions) => ({ BinaryExpression: (path) => processBinary(Object.assign(transOptions, { decimalPkgName: options.decimalPkgName, integer: options.integer, fromNewFunction: options.fromNewFunction, needImport: options.needImport, ownerPath: options.ownerPath }), path) }), options.autoDecimalOptions ); } function handleBinaryError(error) { if (error instanceof Error) { throw new SyntaxError(`AutoDecimal compile error\uFF1A ${error.message}`); } throw error; } function resolveNeedImport(options) { const supportNewFunction = options.autoDecimalOptions.supportNewFunction; if (!options.fromNewFunction || options.fromNewFunction && !supportNewFunction.injectWindow) { options.needImport = true; } } // src/core/traverse/call-expression.ts import { isBinaryExpression as isBinaryExpression2, isIdentifier as isIdentifier2, isMemberExpression as isMemberExpression2, isObjectExpression } from "@babel/types"; function resolveCallExpression(path, options) { const { autoDecimalOptions } = options; const { toDecimal, supportNewFunction } = autoDecimalOptions; if (!toDecimal && !supportNewFunction) return; const { node } = path; const { callee, arguments: args } = node; if (supportNewFunction && isIdentifier2(callee) && callee.name === "Function") { resolveNewFunctionExpression(path, options); return; } if (!isMemberExpression2(callee)) return; const toDecimalOptions = { ...DEFAULT_TO_DECIMAL_CONFIG }; if (toDecimal) { mergeToDecimalOptions(toDecimalOptions, toDecimal); } const { property, object } = callee; if (!isIdentifier2(property) || property.name !== toDecimalOptions.name) return; if (!isBinaryExpression2(path.parentPath.node) && !isBinaryExpression2(object)) { throw new SyntaxError(` line: ${path.parentPath.node.loc?.start.line}, ${options.msa.sliceNode(path.parentPath.node).toString()} \u6216 ${options.msa.sliceNode(object).toString()} \u4E0D\u662F\u6709\u6548\u7684\u8BA1\u7B97\u8868\u8FBE\u5F0F `); } if (args && args.length > 0) { const [arg] = args; if (!isObjectExpression(arg)) { throw new TypeError("toDecimal \u53C2\u6570\u9519\u8BEF"); } const rawArg = options.msa.snipNode(arg).toString(); const jsonArg = rawArg.replace(/(\w+):/g, '"$1":').replace(/'/g, '"'); try { const argToDecimalOptions = JSON.parse(jsonArg); mergeToDecimalOptions(toDecimalOptions, argToDecimalOptions); } catch (e) { console.error(e); } } let callArgs = "()"; if (toDecimalOptions.callMethod === "toFixed") { callArgs = `(${toDecimalOptions.precision}, ${getRoundingMode(toDecimalOptions.roundingModes, autoDecimalOptions.package)})`; } const start = object.end ?? 0; options.msa.remove(start, node.end ?? 0); const resolveBinaryOptions = { ...options, initial: true, callArgs, callMethod: toDecimalOptions.callMethod }; if (isBinaryExpression2(object)) { if (object.start !== node.start) { options.msa.remove(node.start ?? 0, object.start ?? 0); } object.extra = { ...object.extra, __extra: object.extra, options: resolveBinaryOptions, __shouldTransform: true }; return; } const rootPath2 = getRootBinaryExprPath(path); const runtimeOptions = {}; processBinary(Object.assign(runtimeOptions, resolveBinaryOptions), rootPath2); Object.assign(options, { needImport: runtimeOptions.needImport }); } // src/core/traverse/export-declaration.ts import { isBlockStatement, isCallExpression as isCallExpression2, isFunctionExpression as isFunctionExpression2, isIdentifier as isIdentifier3, isObjectExpression as isObjectExpression2, isObjectMethod, isObjectProperty, isReturnStatement, isSpreadElement } from "@babel/types"; function resolveExportDefaultDeclaration(path, options) { let { declaration } = path.node; if (!isObjectExpression2(declaration) && !isCallExpression2(declaration)) return; if (isCallExpression2(declaration)) { const { arguments: args } = declaration; const [objectExpr] = args; if (!objectExpr || !isObjectExpression2(objectExpr)) return; declaration = objectExpr; } const hasDataProperty = existDataProperty(declaration, options); if (!hasDataProperty) { const insertPosition = (declaration.start ?? 0) + 1; const content = ` data() { this.${options.decimalPkgName} = ${options.decimalPkgName}; }, `; options.msa.prependLeft(insertPosition, content); } } function existDataProperty(declaration, options) { const { properties } = declaration; return properties.some((prop) => { if (isSpreadElement(prop)) return false; if (isObjectProperty(prop) && !isFunctionExpression2(prop.value)) return false; if (!isIdentifier3(prop.key) || isIdentifier3(prop.key) && prop.key.name !== "data") return false; const body = isObjectMethod(prop) ? prop.body : prop.value.body; if (!isBlockStatement(body)) return false; const returnStatement = body.body.find((item) => isReturnStatement(item)); if (!returnStatement) return false; const content = ` this.${options.decimalPkgName} = ${options.decimalPkgName}; `; options.msa.prependLeft(returnStatement.start, content); return true; }); } // src/core/traverse/import-declaration.ts import { isIdentifier as isIdentifier4, isImportDefaultSpecifier as isImportDefaultSpecifier2, isImportNamespaceSpecifier as isImportNamespaceSpecifier2 } from "@babel/types"; function resolveImportDeclaration(path, options) { if (path.node.source.value === PKG_NAME) { const defaultDecimalPkgName = options.autoDecimalOptions.decimalName || DECIMAL_PKG_NAME; options.imported = path.node.specifiers.some((spec) => { if (isImportDefaultSpecifier2(spec)) { if (spec.local.name !== defaultDecimalPkgName) { options.decimalPkgName = spec.local.name; } return true; } if (isImportNamespaceSpecifier2(spec)) { const pkgName = options.autoDecimalOptions.package === "big.js" ? "Big" : "Decimal"; options.decimalPkgName = `${spec.local.name}.${pkgName}`; return true; } if (isIdentifier4(spec.imported) && spec.imported.name !== defaultDecimalPkgName) { options.decimalPkgName = spec.local.name; return true; } return false; }); } } // src/core/traverse/new-function.ts import { isArrayExpression, isAssignmentExpression, isCallExpression as isCallExpression3, isIdentifier as isIdentifier5, isMemberExpression as isMemberExpression3, isNodesEquivalent, isNumericLiteral as isNumericLiteral3, isObjectProperty as isObjectProperty2, isReturnStatement as isReturnStatement2, isStatement, isStringLiteral as isStringLiteral2, isVariableDeclarator as isVariableDeclarator2 } from "@babel/types"; function resolveNewFunctionExpression(path, options) { if (!options.autoDecimalOptions.supportNewFunction) return; const { node } = path; const { callee, arguments: args } = node; if (!isIdentifier5(callee) || callee.name !== "Function") return; if (args.length === 0) return; const lastArg = args[args.length - 1]; resolveReturnParam(path, lastArg, options); const { injectWindow } = options.autoDecimalOptions.supportNewFunction; if (!injectWindow) { provideDecimal(path, lastArg, options); } } function resolveReturnParam(path, node, options) { if (isStringNode(node)) { return resolveStringTemplateLiteral(node, options); } if (isIdentifier5(node) || isCallExpression3(node) && isIdentifier5(node.callee)) { const name = isIdentifier5(node) ? node.name : node.callee.name; const binding = getScopeBinding(path, name); resolveVariableParam(options, binding, name); } } function resolveVariableParam(options, binding, name) { if (!binding) return; if (binding.kind === "param") { resolveVariableOfParam(binding, options, name); } const { constantViolations, path } = binding; if (isVariableDeclarator2(path.node)) { const { init } = path.node; if (!init) return; resolveAssignmentExpression(path, init, options); } constantViolations.forEach((cv) => { if (isAssignmentExpression(cv.node)) { const { right } = cv.node; resolveAssignmentExpression(cv, right, options); } }); } function resolveAssignmentExpression(path, node, options) { if (isStringNode(node)) { return resolveStringTemplateLiteral(node, options); } if (isFunctionNode(node)) { return resolveFunction(node, options); } if (isIdentifier5(node)) { const binding = getScopeBinding(path, node.name); return resolveVariableParam(options, binding); } if (isCallExpression3(node)) { const variableName = node.callee.name; const binding = getScopeBinding(path, variableName); if (!binding) return; const pathNode = binding.path.node; if (isFunctionNode(pathNode)) { resolveFunction(pathNode, options); return; } if (isVariableDeclarator2(pathNode) && isFunctionNode(pathNode.init)) { resolveFunction(pathNode.init, options); return; } console.warn(`\u672A\u5904\u7406\u7684\u8282\u70B9\uFF0Cline: ${node.loc.start.line}, ${node.loc.end.index}; column: ${node.loc.start.column}, ${node.loc.end.column}`); } } function resolveVariableOfParam(binding, options, name) { if (!isFunctionNode(binding.scope.block) || !name) { return; } const { block, path } = binding.scope; const { params } = block; if (!params.length) return; const paramsIndex = params.findIndex((param) => param.name === name); if (paramsIndex < 0) return; const fnName = getFunctionName(path); if (!fnName || !path.parentPath) return; const parentBinding = getScopeBinding(path.parentPath, fnName); if (!parentBinding) return; parentBinding.referencePaths.forEach((nodePath) => { if (!isCallExpression3(nodePath.parent)) return; const targetParams = nodePath.parent.arguments[paramsIndex]; if (!targetParams) return; resolveReturnParam(nodePath, targetParams, options); }); } function provideDecimal(path, node, options) { if (!options.msa.hasChanged()) return; let parentPath = path.parentPath; let params; const decimalParamsContent = `'${options.decimalPkgName}', ${options.msa.snipNode(node)}`; const { parent } = path; let callName = ""; if (isCallExpression3(parent)) { options.msa.update(node.start, node.end, decimalParamsContent); options.msa.update(parent.end - 1, parent.end, `, ${options.decimalPkgName})`); return; } if (isAssignmentExpression(parent)) { const { left } = parent; if (isIdentifier5(left)) { callName = left.name; } else if (isMemberExpression3(left)) { const binding2 = getScopeBinding(parentPath, left); if (!binding2) return; binding2.referencePaths.forEach((reference) => { const referenceParent = reference.parentPath.parent; if (isCallExpression3(referenceParent)) { options.msa.update(referenceParent.end - 1, referenceParent.end, `, ${options.decimalPkgName})`); return; } if (isAssignmentExpression(referenceParent)) { const { right } = referenceParent; if (isNodesEquivalent(right, path.node)) { options.msa.update(node.start, node.end, decimalParamsContent); } return; } if (isMemberExpression3(referenceParent)) { const targetPath = getTargetPath(reference, isCallExpression3); if (targetPath) { options.msa.update(targetPath.node.end - 1, targetPath.node.end, `, ${options.decimalPkgName})`); } else { const targetPath2 = getTargetPath(reference, isAssignmentExpression); if (!targetPath2) return; const { right } = targetPath2.node; if (isNodesEquivalent(right, path.node)) { options.msa.update(node.start, node.end, decimalParamsContent); } } } }); return; } } else if (!isVariableDeclarator2(parent)) { parentPath = getTargetPath(path, isVariableDeclarator2); if (!parentPath) return; if (isArrayExpression(parent)) { params = path.key; } else if (isObjectProperty2(parent)) { params = parent.key.name; } } if (!callName) { if (!parentPath || !isVariableDeclarator2(parentPath.node)) { return; } callName = parentPath.node.id?.name; if (!callName) return; } const binding = getScopeBinding(path, callName); if (!binding?.referenced) return; options.msa.update(node.start, node.end, decimalParamsContent); binding.referencePaths.forEach((referencePath) => { const { parent: parent2 } = referencePath; if (isCallExpression3(parent2)) { options.msa.update(parent2.end - 1, parent2.end, `, ${options.decimalPkgName})`); return; } if (isMemberExpression3(parent2)) { if (isNumericLiteral3(parent2.property) || isIdentifier5(parent2.property)) { const targetParams = isNumericLiteral3(parent2.property) ? parent2.property.value : parent2.property.name; if (targetParams !== params) { return; } const targetPath = getTargetPath(referencePath, isCallExpression3); if (!targetPath) return; options.msa.update(targetPath.node.end - 1, targetPath.node.end, `, ${options.decimalPkgName})`); } } }); } function resolveStringTemplateLiteral(node, options) { let rawString = ""; let quote = "'"; if (isStringLiteral2(node)) { rawString = node.value; } else { quote = "`"; rawString = options.msa.snipNode(node).toString().slice(1, -1); } const { autoDecimalOptions } = options; const supportNewFunction = autoDecimalOptions.supportNewFunction; const code = RETURN_DECLARATION_FN.replace(RETURN_DECLARATION_CODE, rawString); const toDecimalParams = supportNewFunction.toDecimal ?? false; const runtimeOptions = {}; const { msa: transformedMsa } = getTransformed(code, (opts) => traverseAst(Object.assign(runtimeOptions, opts, { fromNewFunction: true, needImport: options.needImport }), false), { ...autoDecimalOptions, toDecimal: toDecimalParams }); if (transformedMsa.hasChanged()) { Object.assign(options, { fromNewFunction: runtimeOptions.fromNewFunction, needImport: runtimeOptions.needImport }); const result = transformedMsa.toString().replace(RETURN_DECLARATION_PREFIX, "").slice(0, -1); options.msa.overwriteNode(node, `${quote}${result}${quote}`); } } function resolveFunction(node, options) { const { body } = node; if (isStringNode(body)) { resolveStringTemplateLiteral(body, options); return; } if (isStatement(body)) { const lastNode = body.body[body.body.length - 1]; if (!isReturnStatement2(lastNode)) { return; } const { argument } = lastNode; if (!argument || !isStringNode(argument)) { return; } resolveStringTemplateLiteral(argument, options); } } // src/core/traverse/ast.ts function traverseAst(options, checkImport = true, templateImport = false) { return { enter(path) { switch (path.type) { case "Program": case "ImportDeclaration": case "ExportDefaultDeclaration": case "JSXElement": case "JSXOpeningElement": case "JSXExpressionContainer": case "BinaryExpression": break; default: blockComment(path); nextComment(path); break; } }, Program: { enter(path) { const file = path.parent; const fileIgnore = file.comments?.some((comment) => comment.value.includes(FILE_COMMENT)) ?? false; options.imported = fileIgnore && templateImport; if (fileIgnore && !templateImport) { path.skip(); } }, exit() { const hasChanged = options.msa.hasChanged(); if (!checkImport || options.imported || !hasChanged && !templateImport) { return; } if (!options.needImport) return; const pkgName = options.autoDecimalOptions?.package ?? PKG_NAME; options.imported = true; options.msa.prepend(` import ${options.decimalPkgName} from '${pkgName}'; `); } }, ExportDefaultDeclaration(path) { if (!templateImport) return; resolveExportDefaultDeclaration(path, options); }, ImportDeclaration(path) { if (options.imported) return; resolveImportDeclaration(path, options); }, JSXElement: (path) => innerComment(path, BLOCK_COMMENT), JSXOpeningElement: (path) => { if (!path.node.attributes.length) return; innerComment(path); }, JSXExpressionContainer: (path) => { if (isJSXEmptyExpression2(path.node.expression)) return; innerComment(path); }, BinaryExpression: (path) => resolveBinaryExpression(path, options), CallExpression: (path) => resolveCallExpression(path, options), NewExpression: (path) => resolveNewFunctionExpression(path, options) }; } // src/core/transform.ts function transformAutoDecimal(code, autoDecimalOptions) { const { msa } = getTransformed(code, traverseAst, autoDecimalOptions); return msa; } function transformVueAutoDecimal(code, autoDecimalOptions) { const sfcAst = vueParse(code); const { descriptor } = sfcAst; const { script, scriptSetup, template } = descriptor; const msa = new MagicStringAST(code); const getDecimalPkgName = (scriptSection) => { if (!scriptSection) return autoDecimalOptions.decimalName || DECIMAL_PKG_NAME; const { decimalPkgName: decimalPkgName2 } = getTransformed( scriptSection.content, (options) => ({ ImportDeclaration: (path) => resolveImportDeclaration(path, options) }), autoDecimalOptions ); return decimalPkgName2; }; let decimalPkgName = getDecimalPkgName(scriptSetup); if (!decimalPkgName) { decimalPkgName = getDecimalPkgName(script); } function parserTemplate(children) { const commentState = { line: 0, block: false, next: false }; children.forEach((child) => { if (child.type === NodeTypes.TEXT) return; if (child.type === NodeTypes.COMMENT) { updateCommentState(child, commentState); return; } if (shouldSkipComment(child, commentState, "block")) return; switch (child.type) { case NodeTypes.INTERPOLATION: handleInterpolation(child, commentState); break; case NodeTypes.ELEMENT: handleElementProps(child, commentState); break; default: break; } if (hasChildrenNode(child) && child.children) { parserTemplate(child.children); } }); } function hasChildrenNode(child) { const nodeTypes = [NodeTypes.ELEMENT, NodeTypes.COMPOUND_EXPRESSION, NodeTypes.IF_BRANCH, NodeTypes.FOR]; return nodeTypes.includes(child.type); } function updateCommentState(commentNode, commentState) { commentState.line = commentNode.loc.start.line; commentState.block = commentNode.content.includes(BLOCK_COMMENT); commentState.next = commentNode.content.includes(NEXT_COMMENT); } function handleInterpolation(interpolationNode, commentState) { if (shouldSkipComment(interpolationNode, commentState)) return; if (interpolationNode.content.type === NodeTypes.COMPOUND_EXPRESSION) return; const expContent = interpolationNode.content.content; if (!expContent || !existTargetOperator(expContent)) return; const { msa: transformedMsa } = getTransformed( expContent, (options) => traverseAst({ ...options, decimalPkgName }, false), autoDecimalOptions ); msa.update(interpolationNode.content.loc.start.offset, interpolationNode.content.loc.end.offset, transformedMsa.toString()); } function handleElementProps(elementNode, commentState) { if (shouldSkipComment(elementNode, commentState)) return; if (!elementNode.props.length) return; elementNode.props.forEach((prop) => { if (prop.type === NodeTypes.ATTRIBUTE) return; if (!prop.exp || prop.exp.type === NodeTypes.COMPOUND_EXPRESSION) return; const { loc } = prop.exp; let isObjExpr = false; let content = prop.exp.content; if (!content || !existTargetOperator(content)) return; if (isBuiltInDirective(prop)) return; if (prop.exp.ast && isObjectExpression3(prop.exp.ast)) { isObjExpr = true; content = `${PATCH_DECLARATION}${content}`; } const { msa: transformedMsa } = getTransformed( content, (options) => traverseAst({ ...options, decimalPkgName }, false), autoDecimalOptions ); if (isObjExpr) { transformedMsa.remove(0, PATCH_DECLARATION.length); } msa.update(loc.start.offset, loc.end.offset, transformedMsa.toString()); }); } function isBuiltInDirective(prop) { return ["for", "html", "text"].includes(prop.name); } function existTargetOperator(content) { return OPERATOR_KEYS.some((key) => content.includes(key)); } function shouldSkipComment(child, comment, property = "next") { return comment[property] && comment.line + 1 === child.loc.start.line; } if (template) { const { ast, attrs = {} } = template; if (!attrs["ad-ignore"] && ast?.children) { parserTemplate(ast.children); } } let needsImport = msa.hasChanged(); const parseScript = (scriptSection) => { if (!scriptSection) return; const { start, end } = scriptSection.loc; const { msa: transformedMsa, imported } = getTransformed( scriptSection.content, (options) => traverseAst(options, true, needsImport), autoDecimalOptions ); if (needsImport) { needsImport = !imported; } msa.update(start.offset, end.offset, transformedMsa.toString()); }; parseScript(scriptSetup); parseScript(script); if (needsImport) { msa.append(` <script> import ${DECIMAL_PKG_NAME} from '${PKG_NAME}'; export default { data() { this.${DECIMAL_PKG_NAME} = ${DECIMAL_PKG_NAME}; } } </script> `); } return msa; } function getTransformed(code, traverseOptions, autoDecimalOptions) { const ast = parse(code, { sourceType: "module", plugins: ["typescript", "jsx"] }); const msa = new MagicStringAST(code); const options = { autoDecimalOptions, imported: false, msa, decimalPkgName: autoDecimalOptions.decimalName || DECIMAL_PKG_NAME, initial: false, integer: false, shouldSkip: false, callArgs: "()", callMethod: "toNumber", needImport: false, fromNewFunction: false }; const babelTraverse = traverse.default ?? traverse; babelTraverse(ast, traverseOptions(options)); return options; } // src/core/unplugin.ts function transform(code, id, options) { let msa; if (REGEX_VUE.some((reg) => reg.test(id))) { msa = transformVueAutoDecimal(code, options); } else { msa = transformAutoDecimal(code, options); } if (!msa.hasChanged()) return; return { code: msa.toString(), map: msa.generateMap({ source: id, includeContent: true, hires: true }) }; } var unplugin_default = createUnplugin((rawOptions) => { const filter = createFilter( [REGEX_SUPPORTED_EXT, ...REGEX_VUE], [REGEX_NODE_MODULES] ); const options = resolveOptions(rawOptions); if (options.dts) { generateDeclaration(options); } return { name: "unplugin-auto-decimal", enforce: "pre", transformInclude(id) { return filter(id); }, transform(code, id) { const pkgName = options.package ?? PKG_NAME; if (!isPackageExists2(pkgName)) { console.error(`[AutoDecimal] \u8BF7\u5148\u5B89\u88C5 ${pkgName}`); return { code }; } return transform(code, id, options); } }; }); export { unplugin_default };