v-transform 2.2.3 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -5
- package/src/calibrator.js +135 -0
- package/src/dump.js +40 -11
- package/src/index.js +1 -1
- package/src/parser.js +16 -2
- package/src/prepare.js +5 -2
- package/src/transform.js +37 -16
package/package.json
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
"vt": "src/index.js"
|
|
5
5
|
},
|
|
6
6
|
"type": "module",
|
|
7
|
-
"version": "2.
|
|
7
|
+
"version": "2.3.1",
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"clean": "npx rimraf test",
|
|
11
|
-
"vt": "
|
|
12
|
-
"transform": "
|
|
11
|
+
"vt": "vt",
|
|
12
|
+
"transform": "vt transform"
|
|
13
13
|
},
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"commander": "^15.0.0",
|
|
22
22
|
"glob": "^13.0.6",
|
|
23
|
-
"js-yaml": "^5.2.
|
|
24
|
-
"
|
|
23
|
+
"js-yaml": "^5.2.2",
|
|
24
|
+
"ts-to-zod": "^5.1.0",
|
|
25
|
+
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
|
26
|
+
"zod": "^4.4.3"
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { generate } from 'ts-to-zod'
|
|
4
|
+
|
|
5
|
+
export async function calibrateTsData(data, name, types, pk) {
|
|
6
|
+
// 如果没配对应的 .types.ts 文件,直接退回原始数据,类型签名为 any[]
|
|
7
|
+
if (!types || !fs.existsSync(types)) {
|
|
8
|
+
console.warn(
|
|
9
|
+
`⚠️ 提示: 在 Excel 同级目录下未找到 [${name}.types.ts],将退回普通无类型导出。`,
|
|
10
|
+
)
|
|
11
|
+
const isArray = Array.isArray(data)
|
|
12
|
+
const extra = {
|
|
13
|
+
typeSign: isArray ? 'any[]' : 'Map<any, any>',
|
|
14
|
+
types: isArray ? `export type ${name.toUpperCase()} = any;` : '',
|
|
15
|
+
isNativeMap: !isArray, // 👈 即使降级也需要同步对齐是否为 Map 的标记
|
|
16
|
+
}
|
|
17
|
+
return { data, extra }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.info(
|
|
21
|
+
`🔍 [vTransform TS] 成功在 Excel 目录定位规范,正在校准 [${name}]...`,
|
|
22
|
+
)
|
|
23
|
+
const sourceText = fs.readFileSync(types, 'utf-8')
|
|
24
|
+
const options = { keepOptionalProperties: true }
|
|
25
|
+
const zod = generate({ sourceText, options })
|
|
26
|
+
const zodRawCode = zod
|
|
27
|
+
.getZodSchemasFile()
|
|
28
|
+
.replace(/z\.boolean\(\)/g, 'z.coerce.boolean()')
|
|
29
|
+
const base64Code = Buffer.from(zodRawCode).toString('base64')
|
|
30
|
+
const zodModule = await import(`data:text/javascript;base64,${base64Code}`)
|
|
31
|
+
const targetSchemaName = `${name}Schema`
|
|
32
|
+
const RowZodValidator = zodModule[targetSchemaName]
|
|
33
|
+
|
|
34
|
+
if (!RowZodValidator) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`无法从类型文件中自动推导出名为 ${targetSchemaName} 的主校验器。`,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let cleanData
|
|
41
|
+
let typeSign = ''
|
|
42
|
+
const typeName = name.charAt(0).toUpperCase() + name.slice(1)
|
|
43
|
+
|
|
44
|
+
if (Array.isArray(data)) {
|
|
45
|
+
cleanData = []
|
|
46
|
+
typeSign = `${typeName}[]`
|
|
47
|
+
for (let i = 0; i < data.length; i++) {
|
|
48
|
+
const parseResult = RowZodValidator.safeParse(data[i])
|
|
49
|
+
if (!parseResult.success) {
|
|
50
|
+
printZodError(
|
|
51
|
+
name,
|
|
52
|
+
`第 ${i + 1} 条数据`,
|
|
53
|
+
parseResult.error.issues,
|
|
54
|
+
data[i],
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
cleanData.push(parseResult.data)
|
|
58
|
+
}
|
|
59
|
+
console.info(`📦 [Array 模式] 完美对齐数组规范`)
|
|
60
|
+
}
|
|
61
|
+
// 2. Map 模式验证
|
|
62
|
+
else if (typeof data === 'object' && data !== null) {
|
|
63
|
+
const tempCleanMap = {}
|
|
64
|
+
for (const key in data) {
|
|
65
|
+
const parseResult = RowZodValidator.safeParse(data[key])
|
|
66
|
+
if (!parseResult.success) {
|
|
67
|
+
printZodError(
|
|
68
|
+
name,
|
|
69
|
+
`Key 为 [${key}] 的数据`,
|
|
70
|
+
parseResult.error.issues,
|
|
71
|
+
data[key],
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
tempCleanMap[key] = parseResult.data
|
|
75
|
+
}
|
|
76
|
+
// ========================================================
|
|
77
|
+
// 🌟 终极修复:利用 Zod 原生验证自适应看穿 readonly 伪装
|
|
78
|
+
// ========================================================
|
|
79
|
+
let keyType = 'string'
|
|
80
|
+
if (pk && RowZodValidator.shape && RowZodValidator.shape[pk]) {
|
|
81
|
+
const validator = RowZodValidator.shape[pk]
|
|
82
|
+
|
|
83
|
+
// 💡 核心黑科技:直接用 1(数字)和 "1"(字符串)去该字段的校验器里试探!
|
|
84
|
+
// 无论它套了多少层 readonly() 还是可选约束,只要它不接受数字 1 校验,就说明它不是 number
|
|
85
|
+
if (
|
|
86
|
+
validator.safeParse(1).success &&
|
|
87
|
+
!validator.safeParse('1').success
|
|
88
|
+
) {
|
|
89
|
+
keyType = 'number'
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const isNumKey = keyType === 'number'
|
|
94
|
+
cleanData = Object.entries(tempCleanMap).map(([k, v]) => [
|
|
95
|
+
isNumKey ? Number(k) : k,
|
|
96
|
+
v,
|
|
97
|
+
])
|
|
98
|
+
typeSign = `Map<${keyType}, ${typeName}>`
|
|
99
|
+
console.info(
|
|
100
|
+
`🎯 [Map 模式] 完美对齐原生 Map 规范,主键 [${pk}] 自动识别为: ${keyType}`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 🌟 核心修改 2:在返回的 extra 元数据层,加上 isNativeMap 的明确布尔标记。
|
|
105
|
+
// 这可以让下游的 dump.js 接收到它后,通过简单的一行流判断,决定是将 cleanData 渲染为普通 JSON,还是渲染为强大的 new Map(...)
|
|
106
|
+
const extra = {
|
|
107
|
+
typeSign,
|
|
108
|
+
types: sourceText,
|
|
109
|
+
isNativeMap: !Array.isArray(data), // 如果上游合并出来的不是数组(即Map模式),标记为 true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { data: cleanData, extra }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 统一的美化报错打印
|
|
117
|
+
*/
|
|
118
|
+
function printZodError(sheetName, positionLabel, issues, rawRowData) {
|
|
119
|
+
console.error(`\n❌ [vTransform 数据校准失败]`)
|
|
120
|
+
console.error(
|
|
121
|
+
`👉 发生位置: 表格 [${sheetName}] ${positionLabel}不兼容你的 TS 类型限制!`,
|
|
122
|
+
)
|
|
123
|
+
console.error(`👉 冲突细节详情:`)
|
|
124
|
+
issues.forEach(err => {
|
|
125
|
+
const wrongField = err.path.join('.') || '根节点'
|
|
126
|
+
console.error(` - 错误字段: \x1b[33m${wrongField}\x1b[0m`)
|
|
127
|
+
console.error(` - 期待类型/约束: ${err.message}`)
|
|
128
|
+
console.error(
|
|
129
|
+
` - 当前收到的脏数据值:`,
|
|
130
|
+
rawRowData[wrongField] ?? '缺失字段',
|
|
131
|
+
)
|
|
132
|
+
console.error(` -----------------------------------------`)
|
|
133
|
+
})
|
|
134
|
+
process.exit(1)
|
|
135
|
+
}
|
package/src/dump.js
CHANGED
|
@@ -2,22 +2,47 @@ import { dump as dumpYAML } from 'js-yaml'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { writeFile, stat, mkdir } from 'node:fs/promises'
|
|
4
4
|
|
|
5
|
-
function
|
|
5
|
+
function j(data, space) {
|
|
6
6
|
return JSON.stringify(data, null, space)
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
function
|
|
10
|
-
return
|
|
9
|
+
function jsonify(name, { data, addition }, space) {
|
|
10
|
+
return j(addition ? { [name]: data, ...addition } : data, space)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function
|
|
14
|
-
return `
|
|
13
|
+
function cjsify(name, data, space) {
|
|
14
|
+
return `module.exports = ${jsonify(name, data, space)}`
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
17
|
+
function esmify(name, { data, addition }, space) {
|
|
18
|
+
const main = `export const ${name} = ${jsonify(name, { data, addition }, space)}`
|
|
19
|
+
const def = `export default ${name}`
|
|
20
|
+
if (!addition) return `${main}\n${def}`
|
|
21
|
+
const extra = Object.entries(addition)
|
|
22
|
+
.map(([key, value]) => `export const ${key} = ${j(value, space)}`)
|
|
23
|
+
.join('\n')
|
|
24
|
+
return `${main}\n${extra}\n${def}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function yamlify(name, { data, addition }, indent) {
|
|
28
|
+
const options = { indent: indent || undefined }
|
|
29
|
+
if (!addition) return dumpYAML(data, options)
|
|
30
|
+
return dumpYAML({ [name]: data, ...addition }, options)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function tsify(name, { data, extra, addition }, space) {
|
|
34
|
+
const { types, typeSign, isNativeMap } = extra
|
|
35
|
+
if (!types)
|
|
36
|
+
return `export default ${jsonify(name, { data, addition }, space)}`
|
|
37
|
+
const raw = j(data, space)
|
|
38
|
+
const d = isNativeMap ? `new Map(${raw})` : raw
|
|
39
|
+
const main = `${types}\nexport const ${name} = ${d} as unknown as ${typeSign};`
|
|
40
|
+
const def = `export default ${name}`
|
|
41
|
+
if (!addition) return `${main}\n${def}`
|
|
42
|
+
const extraExports = Object.entries(addition)
|
|
43
|
+
.map(([key, value]) => `export const ${key} = ${j(value, space)}`)
|
|
44
|
+
.join('\n')
|
|
45
|
+
return `${main}\n${extraExports}\n${def}`
|
|
21
46
|
}
|
|
22
47
|
|
|
23
48
|
async function mkdirs(dir) {
|
|
@@ -38,9 +63,13 @@ async function write(sheet, data) {
|
|
|
38
63
|
await writeFile(sheet, data)
|
|
39
64
|
}
|
|
40
65
|
|
|
41
|
-
export async function dump(sheet, data, type, space) {
|
|
66
|
+
export async function dump(sheet, data, type, space, name) {
|
|
42
67
|
let ext, ify
|
|
43
68
|
switch (type) {
|
|
69
|
+
case 'ts':
|
|
70
|
+
ext = '.ts'
|
|
71
|
+
ify = tsify
|
|
72
|
+
break
|
|
44
73
|
case 'cjs':
|
|
45
74
|
ext = '.js'
|
|
46
75
|
ify = cjsify
|
|
@@ -62,5 +91,5 @@ export async function dump(sheet, data, type, space) {
|
|
|
62
91
|
ify = jsonify
|
|
63
92
|
break
|
|
64
93
|
}
|
|
65
|
-
return write(`${sheet}${ext}`, ify(data, space))
|
|
94
|
+
return write(`${sheet}${ext}`, ify(name, data, space))
|
|
66
95
|
}
|
package/src/index.js
CHANGED
|
@@ -11,7 +11,7 @@ program
|
|
|
11
11
|
.command('transform [list...]')
|
|
12
12
|
.option(
|
|
13
13
|
'-t, --type <type>',
|
|
14
|
-
'type of transform, available: js, esm, cjs, json',
|
|
14
|
+
'type of transform, available: js, ts, esm, cjs, json',
|
|
15
15
|
'json',
|
|
16
16
|
)
|
|
17
17
|
.option('-s, --space <space>', 'format space number', 0)
|
package/src/parser.js
CHANGED
|
@@ -9,6 +9,14 @@ function isJsonKey(key) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function processHeaderColumn(col, head, layer, subs, json) {
|
|
12
|
+
// 💡 核心修复:如果该单元格为 null、undefined 或空,直接跳过不处理,防止错位和崩溃
|
|
13
|
+
if (
|
|
14
|
+
head[col] === null ||
|
|
15
|
+
head[col] === undefined ||
|
|
16
|
+
String(head[col]).trim() === ''
|
|
17
|
+
)
|
|
18
|
+
return
|
|
19
|
+
|
|
12
20
|
let key = head[col].trim()
|
|
13
21
|
if (isCommentKey(key)) return
|
|
14
22
|
if (isJsonKey(key)) {
|
|
@@ -134,7 +142,13 @@ function formatSheet(struct, rawSheet, json) {
|
|
|
134
142
|
}
|
|
135
143
|
|
|
136
144
|
export function parser(rawSheet) {
|
|
137
|
-
const
|
|
145
|
+
const head = rawSheet.shift()
|
|
146
|
+
const struct = parseStruct(head)
|
|
138
147
|
rawSheet.shift()
|
|
139
|
-
|
|
148
|
+
const data = formatSheet(struct, rawSheet, struct.json)
|
|
149
|
+
if (struct.key) {
|
|
150
|
+
const col = struct.key.substring(1)
|
|
151
|
+
return [data, head[col].substring(1)]
|
|
152
|
+
}
|
|
153
|
+
return [data, null]
|
|
140
154
|
}
|
package/src/prepare.js
CHANGED
|
@@ -15,8 +15,11 @@ export async function prepare(xlsxPath) {
|
|
|
15
15
|
const datas = []
|
|
16
16
|
for (const name in xlsx.Sheets) {
|
|
17
17
|
const sheetRawData = xlsx.Sheets[name]
|
|
18
|
-
if (!sheetRawData['!ref'])
|
|
19
|
-
const data = utils.sheet_to_json(sheetRawData, {
|
|
18
|
+
if (!sheetRawData['!ref']) continue
|
|
19
|
+
const data = utils.sheet_to_json(sheetRawData, {
|
|
20
|
+
header: 1,
|
|
21
|
+
defval: null,
|
|
22
|
+
})
|
|
20
23
|
datas.push({ name, data })
|
|
21
24
|
}
|
|
22
25
|
return datas
|
package/src/transform.js
CHANGED
|
@@ -3,6 +3,7 @@ import { load } from './loader.js'
|
|
|
3
3
|
import { prepare } from './prepare.js'
|
|
4
4
|
import { parser } from './parser.js'
|
|
5
5
|
import { dump } from './dump.js'
|
|
6
|
+
import { calibrateTsData } from './calibrator.js'
|
|
6
7
|
|
|
7
8
|
export async function transform(options) {
|
|
8
9
|
const now = Date.now()
|
|
@@ -16,21 +17,21 @@ async function task({ files, dest, cwd, type, space, addition }) {
|
|
|
16
17
|
const m = new Map()
|
|
17
18
|
for (const file of files) {
|
|
18
19
|
const dir = path.resolve(dest, path.dirname(file))
|
|
19
|
-
|
|
20
|
-
processPrepared(prepared, dir, m)
|
|
20
|
+
await processPrepared(path.resolve(cwd, file), dir, m, type == 'ts')
|
|
21
21
|
}
|
|
22
22
|
for (const [sheet, job] of m) {
|
|
23
|
-
const data = job.result()
|
|
24
|
-
|
|
25
|
-
else await dump(sheet, data, type, space)
|
|
23
|
+
const data = await job.result()
|
|
24
|
+
await dump(sheet, { ...data, addition }, type, space, job.name)
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
function processPrepared(
|
|
28
|
+
async function processPrepared(src, dir, m, isTs) {
|
|
29
|
+
const prepared = await prepare(src)
|
|
30
|
+
const xlsxDir = path.dirname(src)
|
|
30
31
|
for (const { name, data } of prepared) {
|
|
31
32
|
if (name.startsWith('#')) continue
|
|
32
|
-
const { sheet, keys } = parseSheetAndKeys(name, dir)
|
|
33
|
-
if (!m.has(sheet)) m.set(sheet, new JobData())
|
|
33
|
+
const { sheet, keys, name: parsedName } = parseSheetAndKeys(name, dir)
|
|
34
|
+
if (!m.has(sheet)) m.set(sheet, new JobData(isTs, parsedName, xlsxDir))
|
|
34
35
|
m.get(sheet).append({ keys, data: parser(data) })
|
|
35
36
|
}
|
|
36
37
|
}
|
|
@@ -40,22 +41,30 @@ function parseSheetAndKeys(name, dir) {
|
|
|
40
41
|
sheet = sheet.replace('<arr>', '')
|
|
41
42
|
if (sheet.startsWith('>')) sheet = sheet.substring(1)
|
|
42
43
|
const keys = sheet.split('.')
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
let key = keys.shift()
|
|
45
|
+
sheet = path.resolve(dir, key)
|
|
46
|
+
return { sheet, keys, name: key }
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
class JobData {
|
|
48
|
-
constructor(
|
|
49
|
-
|
|
50
|
+
constructor(isTs, name, xlsxDir) {
|
|
51
|
+
this.#isTs = isTs
|
|
52
|
+
this.#name = name
|
|
53
|
+
this.#xlsxDir = xlsxDir
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
#data = []
|
|
57
|
+
#pk = null
|
|
58
|
+
#isTs = false
|
|
59
|
+
#name = ''
|
|
60
|
+
#xlsxDir = ''
|
|
53
61
|
|
|
54
|
-
append(data) {
|
|
55
|
-
this.#data.push(data)
|
|
62
|
+
append({ data: [data, pk], keys }) {
|
|
63
|
+
this.#data.push({ data, keys })
|
|
64
|
+
if (pk) this.#pk = pk
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
result() {
|
|
67
|
+
async result() {
|
|
59
68
|
if (!this.#data.length) return {}
|
|
60
69
|
let result
|
|
61
70
|
for (const { keys, data } of this.#data) {
|
|
@@ -72,7 +81,15 @@ class JobData {
|
|
|
72
81
|
}
|
|
73
82
|
r[last] = this.#combine(r[last], data)
|
|
74
83
|
}
|
|
75
|
-
return result
|
|
84
|
+
if (!this.#isTs) return { data: result }
|
|
85
|
+
|
|
86
|
+
// TODO: 这里调用数据清洗
|
|
87
|
+
return await calibrateTsData(
|
|
88
|
+
result,
|
|
89
|
+
this.#name,
|
|
90
|
+
path.join(this.#xlsxDir, `${this.#name}.types.ts`),
|
|
91
|
+
this.#pk,
|
|
92
|
+
)
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
#combine(a, b) {
|
|
@@ -90,4 +107,8 @@ class JobData {
|
|
|
90
107
|
for (const key in b) if (!result[key]) result[key] = b[key]
|
|
91
108
|
return result
|
|
92
109
|
}
|
|
110
|
+
|
|
111
|
+
get name() {
|
|
112
|
+
return this.#name
|
|
113
|
+
}
|
|
93
114
|
}
|