Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/nutui-replace-icons/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
types
node_modules
56 changes: 56 additions & 0 deletions packages/nutui-replace-icons/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## 功能说明

用于替换组件库内置的 Icon 库。

## 使用方法

### Taro 生态下

1. Taro 环境中需要在 config/index.js 文件中增加如下代码

```html
{ h5: { compile: { include: [path.resolve(__dirname, '../node_modules')], } },
mini: { compile: { include: [path.resolve(__dirname, '../node_modules')], } } }
```

2. 在 babel.config.js 文件中增加如下代码

```js
const { repleaceIcons } = require('@nutui/replace-icons')
{
plugins: [
[
repleaceIcons({
targetIconLibary: '@test/aa',
iconMap: {
Loading: Star,
},
}),
],
]
}
```

### H5 生态下

1. 需要将 nutui 组件库包含在编译中。
Comment on lines +34 to +36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

H5环境配置说明不完整

H5环境的配置说明过于简单,缺少具体的配置步骤和示例代码。建议补充:

  1. babel配置示例
  2. webpack/vite配置示例
  3. 完整的使用步骤

需要我帮您补充H5环境的详细配置说明吗?


### 原始代码

```jsx
import { Loading } from '@nutui/icons-react'

export default () => {
return <Loading />
}
```

### 替换后代码

```jsx
import { Star as Loading } from '@test/aa'

export default () => {
return <Loading />
}
```
56 changes: 56 additions & 0 deletions packages/nutui-replace-icons/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@nutui/replace-icons",
"version": "1.0.2",
"description": "",
"keywords": [
"Taro",
"nutui",
"nutui react",
"nutui icons",
"Plugin"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"author": "",
"homepage": "",
"license": "MIT",
"main": "dist/index.js",
"typings": "types/index.d.ts",
"files": [
"dist",
"index.js",
"types"
],
"repository": {
"type": "git",
"url": ""
},
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"test": "vitest"
},
"bugs": {
"url": ""
},
"dependencies": {
"@babel/cli": "^7.25.7",
"@babel/core": "^7.23.9",
"@babel/generator": "^7.24.5",
"@babel/preset-env": "^7.25.7",
"@babel/preset-react": "^7.25.7",
"@types/babel__core": "^7.20.5",
"@types/babel__generator": "^7.6.8",
"vitest": "^1.5.0"
},
"devDependencies": {
"@types/lodash.kebabcase": "^4.1.9",
"@types/node": "^18.13.0",
"prettier": "^3.2.5",
"rollup": "^2.79.0",
"rollup-plugin-ts": "^3.0.2",
"typescript": "^5.4.5"
}
}
22 changes: 22 additions & 0 deletions packages/nutui-replace-icons/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const path = require('path')
const ts = require('rollup-plugin-ts')

const cwd = __dirname

const base = {
external: ['@tarojs/service'],
plugins: [ts()],
}

// 供 CLI 编译时使用的 Taro 插件入口
const compileConfig = {
input: path.join(cwd, 'src/index.ts'),
output: {
file: path.join(cwd, 'dist/index.js'),
format: 'cjs',
sourcemap: true,
},
...base,
}

module.exports = [compileConfig]
3 changes: 3 additions & 0 deletions packages/nutui-replace-icons/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { replaceIcons } from './replace-icons'

export default replaceIcons
35 changes: 35 additions & 0 deletions packages/nutui-replace-icons/src/replace-icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { IOptions } from './type'

function replace(options: IOptions) {
const sourceLibrary = options.sourceLibrary || []
const targetLibrary = options.targetLibrary
return {
visitor: {
ImportDeclaration(path) {
if (sourceLibrary.indexOf(path.node.source.value) > -1) {
// 替换包名称
path.node.source.value = targetLibrary
path.node.specifiers.forEach((specifier) => {
// 根据 iconMappings 进行替换
const iconMappings = options.iconMappings
if (iconMappings && iconMappings[specifier.imported.name]) {
specifier.imported.name = iconMappings[specifier.imported.name]
}
})
}
},
},
}
}
Comment on lines +3 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

代码可以进行以下优化

当前实现有以下几点可以改进:

  1. 使用更现代的数组方法
  2. 增加错误处理
  3. 使用可选链
  4. 添加英文注释以保持一致性

建议按照以下方式重构:

 function replace(options: IOptions) {
   const sourceLibrary = options.sourceLibrary || []
   const targetLibrary = options.targetLibrary
   return {
     visitor: {
       ImportDeclaration(path) {
-        if (sourceLibrary.indexOf(path.node.source.value) > -1) {
+        if (sourceLibrary.includes(path.node.source.value)) {
-          // 替换包名称
+          // Replace package name
           path.node.source.value = targetLibrary
           path.node.specifiers.forEach((specifier) => {
-            // 根据 iconMappings 进行替换
+            // Replace based on iconMappings
             const iconMappings = options.iconMappings
-            if (iconMappings && iconMappings[specifier.imported.name]) {
+            if (iconMappings?.[specifier.imported.name]) {
               specifier.imported.name = iconMappings[specifier.imported.name]
+            } else {
+              console.warn(`No mapping found for icon: ${specifier.imported.name}`)
             }
           })
         }
       },
     },
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function replace(options: IOptions) {
const sourceLibrary = options.sourceLibrary || []
const targetLibrary = options.targetLibrary
return {
visitor: {
ImportDeclaration(path) {
if (sourceLibrary.indexOf(path.node.source.value) > -1) {
// 替换包名称
path.node.source.value = targetLibrary
path.node.specifiers.forEach((specifier) => {
// 根据 iconMappings 进行替换
const iconMappings = options.iconMappings
if (iconMappings && iconMappings[specifier.imported.name]) {
specifier.imported.name = iconMappings[specifier.imported.name]
}
})
}
},
},
}
}
function replace(options: IOptions) {
const sourceLibrary = options.sourceLibrary || []
const targetLibrary = options.targetLibrary
return {
visitor: {
ImportDeclaration(path) {
if (sourceLibrary.includes(path.node.source.value)) {
// Replace package name
path.node.source.value = targetLibrary
path.node.specifiers.forEach((specifier) => {
// Replace based on iconMappings
const iconMappings = options.iconMappings
if (iconMappings?.[specifier.imported.name]) {
specifier.imported.name = iconMappings[specifier.imported.name]
} else {
console.warn(`No mapping found for icon: ${specifier.imported.name}`)
}
})
}
},
},
}
}
🧰 Tools
🪛 Biome

[error] 15-16: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


export function replaceIcons(
options: IOptions = {
sourceLibrary: ['@nutui/icons-react', '@nutui/icons-react-taro'],
targetLibrary: '',
}
) {
if (!options.targetLibrary) {
return {}
}
return replace(options)
}
Comment on lines +25 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

建议增强输入验证和错误处理

当前实现在参数验证和错误处理方面较为简单,建议增加以下功能:

  1. 验证 sourceLibrary 是否为非空数组
  2. 验证 targetLibrary 的格式
  3. 验证 iconMappings 的结构

建议修改如下:

 export function replaceIcons(
   options: IOptions = {
     sourceLibrary: ['@nutui/icons-react', '@nutui/icons-react-taro'],
     targetLibrary: '',
   }
 ) {
+  // Validate sourceLibrary
+  if (!Array.isArray(options.sourceLibrary) || options.sourceLibrary.length === 0) {
+    throw new Error('sourceLibrary must be a non-empty array')
+  }
+
+  // Validate targetLibrary
   if (!options.targetLibrary) {
-    return {}
+    throw new Error('targetLibrary is required')
   }
+
+  // Validate iconMappings if provided
+  if (options.iconMappings && typeof options.iconMappings !== 'object') {
+    throw new Error('iconMappings must be an object')
+  }
+
   return replace(options)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function replaceIcons(
options: IOptions = {
sourceLibrary: ['@nutui/icons-react', '@nutui/icons-react-taro'],
targetLibrary: '',
}
) {
if (!options.targetLibrary) {
return {}
}
return replace(options)
}
export function replaceIcons(
options: IOptions = {
sourceLibrary: ['@nutui/icons-react', '@nutui/icons-react-taro'],
targetLibrary: '',
}
) {
// Validate sourceLibrary
if (!Array.isArray(options.sourceLibrary) || options.sourceLibrary.length === 0) {
throw new Error('sourceLibrary must be a non-empty array')
}
// Validate targetLibrary
if (!options.targetLibrary) {
throw new Error('targetLibrary is required')
}
// Validate iconMappings if provided
if (options.iconMappings && typeof options.iconMappings !== 'object') {
throw new Error('iconMappings must be an object')
}
return replace(options)
}

5 changes: 5 additions & 0 deletions packages/nutui-replace-icons/src/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IOptions {
sourceLibrary: string[]
targetLibrary: string
Comment on lines +2 to +3
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

考虑支持多个目标图标库

目前 targetLibrary 只支持单个目标库,这可能会限制插件的灵活性。建议考虑将其改为数组类型,以支持多个目标库的场景。

建议修改如下:

 export interface IOptions {
   sourceLibrary: string[]
-  targetLibrary: string
+  targetLibrary: string[]
   iconMappings?: { [key: string]: string }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sourceLibrary: string[]
targetLibrary: string
sourceLibrary: string[]
targetLibrary: string[]

iconMappings?: { [key: string]: string }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`replace Loading icons with Star 1`] = `
"import { Star as Loading } from "@test/aa";
import { ArrowSize6 as Arrow } from "@test/aa";
const ReplaceOne = () => {
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Loading, null), " ", /*#__PURE__*/React.createElement(Arrow, null));
};"
`;
29 changes: 29 additions & 0 deletions packages/nutui-replace-icons/test/replace-case.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import babel from '@babel/core'
import { describe, expect, it } from 'vitest'
import { replaceIcons } from '../src/replace-icons'

const plugin = replaceIcons({
targetLibrary: '@test/aa',
iconMappings: {
Loading: 'Star',
},
})
Comment on lines +5 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

建议添加类型定义以增强类型安全性

建议为配置对象添加接口定义,可以防止配置错误并提供更好的开发体验。

+interface IconConfig {
+  targetLibrary: string;
+  iconMappings: Record<string, string>;
+}

-const plugin = replaceIcons({
+const plugin = replaceIcons<IconConfig>({
   targetLibrary: '@test/aa',
   iconMappings: {
     Loading: 'Star',
   },
})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const plugin = replaceIcons({
targetLibrary: '@test/aa',
iconMappings: {
Loading: 'Star',
},
})
interface IconConfig {
targetLibrary: string;
iconMappings: Record<string, string>;
}
const plugin = replaceIcons<IconConfig>({
targetLibrary: '@test/aa',
iconMappings: {
Loading: 'Star',
},
})


const babelOptions = {
presets: ['@babel/preset-react'],
plugins: [plugin],
}
const caseIns = `
import { Loading } from '@nutui/icons-react'
import { ArrowSize6 as Arrow } from '@nutui/icons-react'
const ReplaceOne = () => {
return <><Loading /> <Arrow /></>
}
`
Comment on lines +16 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

建议改进测试用例的可维护性

建议将测试用例代码提取为单独的常量或文件,这样可以更好地管理多个测试用例。

+const TEST_CASES = {
+  basicReplacement: `
   import { Loading } from '@nutui/icons-react'
   import { ArrowSize6 as Arrow } from '@nutui/icons-react'
   const ReplaceOne = () => {
     return <><Loading /> <Arrow /></>
   }
+  `,
+};

-const caseIns = \`...\`
+const caseIns = TEST_CASES.basicReplacement;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const caseIns = `
import { Loading } from '@nutui/icons-react'
import { ArrowSize6 as Arrow } from '@nutui/icons-react'
const ReplaceOne = () => {
return <><Loading /> <Arrow /></>
}
`
const TEST_CASES = {
basicReplacement: `
import { Loading } from '@nutui/icons-react'
import { ArrowSize6 as Arrow } from '@nutui/icons-react'
const ReplaceOne = () => {
return <><Loading /> <Arrow /></>
}
`,
};
const caseIns = TEST_CASES.basicReplacement;

describe('', () => {
it('replace Loading icons with Star', () => {
const ast = babel.transformSync(caseIns, babelOptions)
// @ts-ignore
expect(ast.code).toMatchSnapshot()
})
})
Comment on lines +23 to +29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

测试实现需要多处改进

  1. describe 块缺少描述
  2. 应避免使用 @ts-ignore
  3. 建议补充具体的断言来增强测试可靠性

建议按如下方式修改:

-describe('', () => {
+describe('replaceIcons Plugin', () => {
   it('replace Loading icons with Star', () => {
     const ast = babel.transformSync(caseIns, babelOptions)
-    // @ts-ignore
-    expect(ast.code).toMatchSnapshot()
+    const result = ast?.code ?? '';
+    expect(result).toMatchSnapshot()
+    // 添加具体断言
+    expect(result).toContain('@test/aa')
+    expect(result).toContain('Star')
+    expect(result).not.toContain('Loading')
   })
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('', () => {
it('replace Loading icons with Star', () => {
const ast = babel.transformSync(caseIns, babelOptions)
// @ts-ignore
expect(ast.code).toMatchSnapshot()
})
})
describe('replaceIcons Plugin', () => {
it('replace Loading icons with Star', () => {
const ast = babel.transformSync(caseIns, babelOptions)
const result = ast?.code ?? '';
expect(result).toMatchSnapshot()
// 添加具体断言
expect(result).toContain('@test/aa')
expect(result).toContain('Star')
expect(result).not.toContain('Loading')
})
})

27 changes: 27 additions & 0 deletions packages/nutui-replace-icons/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"noImplicitAny": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"removeComments": false,
"resolveJsonModule": true,
"skipLibCheck": true,
"strictNullChecks": true,
"target": "ES2015",
"outDir": "./dist",
"rootDir": "./src",
"module": "ESNext",
"sourceMap": true,
"declaration": true,
"declarationDir": "types",
"isolatedModules": false,
"types": ["node"]
},
"include": [
"./src"
]
}
Loading
Loading