Skip to content

Commit ac34b88

Browse files
committed
新增 matcher 用于自定义处理匹配到的文件。
1 parent 2da0211 commit ac34b88

File tree

7 files changed

+367
-116
lines changed

7 files changed

+367
-116
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
node_modules
22

33
temp
4+
5+
log

README.md

+87-39
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- transformImage 批量图片处理
66
- replaceFileContent 批量文件替换内容
7+
- matcher 自定义处理匹配到的文件
78

89
### transformImage
910

@@ -13,37 +14,37 @@
1314

1415
##### TransformImage
1516

16-
| 选项 | 类型 | 描述 |
17-
| --- | --- | --- |
18-
| entry | string | 图片入口目录 |
19-
| output | string | 图片出口目录 |
20-
| rules | Rule[] | 匹配图片规则 |
21-
| useFile | UseFile | 匹配被文件内用到的图片 |
22-
| mkdir | boolean | 是否创建文件夹 |
23-
| logFileGeneratePath | string | 日志文件存储路径 |
24-
| itemLog | boolean | 是否将每项的路径打印 |
17+
| 选项 | 类型 | 描述 |
18+
| ------------------- | ------- | ---------------------- |
19+
| entry | string | 图片入口目录 |
20+
| output | string | 图片出口目录 |
21+
| rules | Rule[] | 匹配图片规则 |
22+
| useFile | UseFile | 匹配被文件内用到的图片 |
23+
| mkdir | boolean | 是否创建文件夹 |
24+
| logFileGeneratePath | string | 日志文件存储路径 |
25+
| itemLog | boolean | 是否将每项的路径打印 |
2526

2627
##### Rule
2728

28-
| 选项 | 类型 | 描述 |
29-
| --- | --- | --- |
30-
| name | 'png' or 'jpg' | 匹配的图片格式 |
31-
| format | Format | 如何处理 |
29+
| 选项 | 类型 | 描述 |
30+
| ------ | -------------- | -------------- |
31+
| name | 'png' or 'jpg' | 匹配的图片格式 |
32+
| format | Format | 如何处理 |
3233

3334
#### Format
3435

35-
| 选项 | 类型 | 描述 |
36-
| --- | --- | --- |
37-
| name | 'webp' or 'svg' | 要转变的图片格式 |
38-
| max | number | 图片的最大体质 |
39-
| min | number | 图片的最小体积 |
40-
| handle | (info: { rawName: R, formatName: F, entryPath: string, outputPath: string, size: number }) => Promise<any> | 处理函数 |
36+
| 选项 | 类型 | 描述 |
37+
| ------ | ---------------------------------------------------------------------------------------------------------- | ---------------- |
38+
| name | 'webp' or 'svg' | 要转变的图片格式 |
39+
| max | number | 图片的最大体质 |
40+
| min | number | 图片的最小体积 |
41+
| handle | (info: { rawName: R, formatName: F, entryPath: string, outputPath: string, size: number }) => Promise<any> | 处理函数 |
4142

4243
#### UseFile
4344

44-
| 选项 | 类型 | 描述 |
45-
| --- | --- | --- |
46-
| dir | string | 文件入口目录 |
45+
| 选项 | 类型 | 描述 |
46+
| ---------------- | ---------------- | ------------------ |
47+
| dir | string | 文件入口目录 |
4748
| imageInFileAlias | Record<any, any> | 图片在文件内的别名 |
4849

4950
### replaceFileContent
@@ -52,20 +53,67 @@
5253

5354
#### 参数
5455

55-
##### ReplaceFileContent
56-
57-
| 选项 | 类型 | 描述 |
58-
| --- | --- | --- |
59-
| entry | string | 文件入口目录 |
60-
| list | ReplaceInfo[] | 替换选项 |
61-
| logFileGeneratePath | string | 日志文件存储路径 |
62-
| itemLog | boolean | 是否将每项的路径打印 |
63-
64-
##### ReplaceInfo
65-
66-
| 选项 | 类型 | 描述 |
67-
| --- | --- | --- |
68-
| searchValue | string | 匹配值 |
69-
| replaceValue | List | 替换值 |
70-
71-
56+
##### ReplaceFileContent
57+
58+
| 选项 | 类型 | 描述 |
59+
| ------------------- | ------------- | -------------------- |
60+
| entry | string | 文件入口目录 |
61+
| list | ReplaceInfo[] | 替换选项 |
62+
| logFileGeneratePath | string | 日志文件存储路径 |
63+
| itemLog | boolean | 是否将每项的路径打印 |
64+
65+
##### ReplaceInfo
66+
67+
| 选项 | 类型 | 描述 |
68+
| ------------ | ------ | ------ |
69+
| searchValue | string | 匹配值 |
70+
| replaceValue | List | 替换值 |
71+
72+
### matcher
73+
74+
自定义处理匹配到的文件。用法跟 webpack 相似。
75+
76+
**示例**
77+
78+
以更新大量组件的路径和 props 为例:
79+
80+
```ts
81+
import matcher, {
82+
filterByContent,
83+
insertToLastImport,
84+
replaceContent
85+
} from '/src/matcher'
86+
87+
matcher({
88+
entry: 'e:/HXL/工具/batch-handle-resource/test/data',
89+
rules: [
90+
{
91+
// 匹配 entery 文件夹下所有的 js 文件
92+
match: /\.js$/,
93+
use: [
94+
// filterByContent 用于过滤掉不存在该内容的文件
95+
filterByContent("import Header from 'src/components/Header/Header'"),
96+
// insertToLastImport 用于将该内容插入到最后一个 import 语句下面
97+
insertToLastImport(
98+
"import { headerDictionaries } from 'src/components/i18n/header/config/i18n/en'"
99+
),
100+
// replaceContent 用于将替换文件内容
101+
replaceContent(
102+
[
103+
{
104+
searchValue: "import Header from 'src/components/Header/Header'",
105+
replaceValue: "import Header from 'src/components/i18n/header'"
106+
},
107+
{
108+
searchValue: '<Header />',
109+
replaceValue:
110+
'<Header dictionaries={headerDictionaries} styles={headerStyles} />'
111+
}
112+
],
113+
true
114+
)
115+
]
116+
}
117+
]
118+
})
119+
```

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"main": "src/index.ts",
66
"scripts": {
77
"start": "tsx src/index.ts",
8-
"test": "tsx test/index.ts"
8+
"test": "tsx test/index.ts",
9+
"i18n-use-update": "tsx scripts/i18n-use-update.ts"
910
},
1011
"keywords": [],
1112
"author": "CoderHXL",

scripts/i18n-use-update.ts

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import matcher, {
2+
filterByContent,
3+
insertToLastImport,
4+
replaceContent,
5+
log,
6+
Context
7+
} from '../src/matcher'
8+
9+
// default: en
10+
const local = ['de', 'es', 'fr', 'it', 'jp', 'kr', 'pt']
11+
12+
export function customInsertToLastImport(
13+
insertValue: string
14+
): (context: Context) => Promise<Context> {
15+
return async (context: Context) => {
16+
const endPath = local.find((v) => context.path.includes(v)) ?? 'en'
17+
insertValue += endPath
18+
19+
if (!context.content.includes('import ')) {
20+
context.content = insertValue + context.content
21+
22+
return context
23+
}
24+
25+
const splitByImport = context.content
26+
.split('import ')
27+
.splice(1)
28+
.map((item) => 'import ' + item)
29+
30+
const endChunk = splitByImport[splitByImport.length - 1]
31+
32+
const regex = /['"]([^'"]+)['"]/
33+
const found = endChunk.match(regex)
34+
if (found) {
35+
const moduleName = found[0]
36+
const splitByModuleName = endChunk.split(moduleName)
37+
splitByModuleName[0] += moduleName + '\n'
38+
39+
splitByImport[splitByImport.length - 1] = [
40+
splitByModuleName[0],
41+
insertValue,
42+
splitByModuleName[1]
43+
].join('')
44+
}
45+
46+
context.content = splitByImport.join('')
47+
48+
return context
49+
}
50+
}
51+
52+
const importStr = `
53+
import {
54+
headerDictionaries,
55+
headerStyles
56+
} from 'src/components/i18n/header/config/i18n/'\n
57+
`
58+
59+
const logTitle = `# page - 更新组件路径和 Props
60+
**import Header from "src/components/Header/Header" => import Header from "src/components/i18n/header"**
61+
`
62+
63+
matcher({
64+
entry: 'e:/HXL/工具/batch-handle-resource/test/data',
65+
rules: [
66+
{
67+
match: /\.js$/,
68+
use: [
69+
filterByContent('import Header from "src/components/Header/Header"'),
70+
customInsertToLastImport(importStr),
71+
replaceContent(
72+
[
73+
{
74+
searchValue: 'import Header from "src/components/Header/Header"',
75+
replaceValue: 'import Header from "src/components/i18n/header"'
76+
},
77+
{
78+
searchValue: '<Header />',
79+
replaceValue:
80+
'<Header dictionaries={headerDictionaries} styles={headerStyles} />'
81+
}
82+
],
83+
false
84+
),
85+
log({
86+
name: 'page-更新组件路径和Props',
87+
title: logTitle,
88+
header: ['次数', '路径'],
89+
storePath: 'e:/HXL/工具/batch-handle-resource/log'
90+
})
91+
]
92+
}
93+
]
94+
})

src/matcher/handle.ts

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import fs from 'fs'
2+
3+
import { Context } from './'
4+
import { LogFileGenerate } from '../shared'
5+
6+
export function filterByContent(
7+
target: string
8+
): (context: Context) => Promise<Context> {
9+
return async (context: Context) => {
10+
context.isFilter = !context.content.includes(target)
11+
12+
return context
13+
}
14+
}
15+
16+
export function insertToLastImport(
17+
insertValue: string
18+
): (context: Context) => Promise<Context> {
19+
return async (context: Context) => {
20+
if (!context.content.includes('import ')) {
21+
context.content = insertValue + context.content
22+
23+
return context
24+
}
25+
26+
const splitByImport = context.content
27+
.split('import ')
28+
.splice(1)
29+
.map((item) => 'import ' + item)
30+
31+
const endChunk = splitByImport[splitByImport.length - 1]
32+
33+
const regex = /['"]([^'"]+)['"]/
34+
const found = endChunk.match(regex)
35+
if (found) {
36+
const moduleName = found[0]
37+
const splitByModuleName = endChunk.split(moduleName)
38+
splitByModuleName[0] += moduleName + '\n'
39+
40+
splitByImport[splitByImport.length - 1] = [
41+
splitByModuleName[0],
42+
insertValue,
43+
splitByModuleName[1]
44+
].join('')
45+
}
46+
47+
context.content = splitByImport.join('')
48+
49+
return context
50+
}
51+
}
52+
53+
export function replaceContent(
54+
replaceList: { searchValue: string | RegExp; replaceValue: string }[],
55+
isWiteFile: boolean = false
56+
): (context: Context) => Promise<Context> {
57+
return async (context) => {
58+
for (const item of replaceList) {
59+
const { searchValue, replaceValue } = item
60+
context.content = context.content.replaceAll(searchValue, replaceValue)
61+
}
62+
63+
if (isWiteFile) await fs.promises.writeFile(context.path, context.content)
64+
65+
return context
66+
}
67+
}
68+
69+
export function log(config: {
70+
name: string
71+
title: string
72+
header: string[]
73+
storePath: string
74+
}): (context: Context) => Promise<Context> {
75+
const logFile = new LogFileGenerate(config.name, config.title, config.header)
76+
77+
let i = 0
78+
let id: undefined | NodeJS.Timeout = undefined
79+
return async (context) => {
80+
logFile.addRow([++i, context.path])
81+
82+
if (id) {
83+
clearTimeout(id)
84+
}
85+
id = setTimeout(() => {
86+
logFile.generate(config.storePath)
87+
}, 500)
88+
89+
return context
90+
}
91+
}

src/matcher/index.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { getFilePath, getFileContentAndPath } from '../shared'
2+
3+
export interface Context {
4+
path: string
5+
content: string
6+
isFilter: boolean
7+
}
8+
9+
interface matcher {
10+
entry: string
11+
rules: {
12+
match: RegExp
13+
use: ((info: Context) => Promise<Context>)[]
14+
}[]
15+
}
16+
17+
export default async function matcher(config: matcher) {
18+
const { entry, rules } = config
19+
20+
const paths = getFilePath(
21+
entry,
22+
(filePath) => !!rules.find((n) => filePath.match(n.match))
23+
)
24+
25+
const fileInfos = await getFileContentAndPath(paths)
26+
27+
for (const fileInfo of fileInfos) {
28+
const { path, content } = fileInfo
29+
const rule = rules.find((n) => path.match(n.match))
30+
31+
if (!rule) continue
32+
33+
const context: Context = { path, content, isFilter: false }
34+
for (const fn of rule.use) {
35+
if (context.isFilter) break
36+
await fn(context)
37+
}
38+
}
39+
40+
return paths
41+
}
42+
43+
export * from './handle'

0 commit comments

Comments
 (0)