v-transform 2.2.2 → 2.3.0

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 CHANGED
@@ -4,12 +4,12 @@
4
4
  "vt": "src/index.js"
5
5
  },
6
6
  "type": "module",
7
- "version": "2.2.2",
7
+ "version": "2.3.0",
8
8
  "main": "index.js",
9
9
  "scripts": {
10
10
  "clean": "npx rimraf test",
11
- "vt": "node src/index.js",
12
- "transform": "node src/index.js 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.0",
24
- "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
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
@@ -1,23 +1,48 @@
1
- import yaml from 'js-yaml'
1
+ 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 jsonify(data, space) {
5
+ function j(data, space) {
6
6
  return JSON.stringify(data, null, space)
7
7
  }
8
8
 
9
- function cjsify(data, space) {
10
- return `module.exports = ${jsonify(data, space)}`
9
+ function jsonify(name, { data, addition }, space) {
10
+ return j(addition ? { [name]: data, ...addition } : data, space)
11
11
  }
12
12
 
13
- function esmify(data, space) {
14
- return `export default ${jsonify(data, space)}`
13
+ function cjsify(name, data, space) {
14
+ return `module.exports = ${jsonify(name, data, space)}`
15
15
  }
16
16
 
17
- function yamlify(data, space) {
18
- return yaml.dump(data, {
19
- indent: space || undefined,
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/loader.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { glob } from 'glob'
2
- import yaml from 'js-yaml'
2
+ import { load as loadYAML } from 'js-yaml'
3
3
  import path from 'node:path'
4
4
  import { readFile } from 'node:fs/promises'
5
5
 
@@ -51,7 +51,7 @@ async function loadConfig(config) {
51
51
  case '.json':
52
52
  case '.yaml':
53
53
  case '.yml':
54
- return yaml.load(await readFile(config))
54
+ return loadYAML(await readFile(config))
55
55
  case '.js':
56
56
  case '.mjs':
57
57
  return (await import(config)).default
package/src/parser.js CHANGED
@@ -1,4 +1,4 @@
1
- import yaml from 'js-yaml'
1
+ import { load } from 'js-yaml'
2
2
 
3
3
  function isCommentKey(key) {
4
4
  return key.startsWith('#')
@@ -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)) {
@@ -88,7 +96,7 @@ function formatRow({ type, key, source, subs }, row, original, json) {
88
96
 
89
97
  function formatValue(key, source, value, json) {
90
98
  if (value == null) return null
91
- if (json.includes(source)) value = yaml.load(value)
99
+ if (json.includes(source)) value = load(value)
92
100
  return { key, value }
93
101
  }
94
102
 
@@ -134,7 +142,13 @@ function formatSheet(struct, rawSheet, json) {
134
142
  }
135
143
 
136
144
  export function parser(rawSheet) {
137
- const struct = parseStruct(rawSheet.shift())
145
+ const head = rawSheet.shift()
146
+ const struct = parseStruct(head)
138
147
  rawSheet.shift()
139
- return formatSheet(struct, rawSheet, struct.json)
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']) break
19
- const data = utils.sheet_to_json(sheetRawData, { header: 1 })
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
- const prepared = await prepare(path.resolve(cwd, file))
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
- if (addition) await dump(sheet, { data, ...addition }, type, space)
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(prepared, dir, m) {
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
33
  const { sheet, keys } = parseSheetAndKeys(name, dir)
33
- if (!m.has(sheet)) m.set(sheet, new JobData())
34
+ if (!m.has(sheet)) m.set(sheet, new JobData(isTs, name, xlsxDir))
34
35
  m.get(sheet).append({ keys, data: parser(data) })
35
36
  }
36
37
  }
@@ -45,17 +46,24 @@ function parseSheetAndKeys(name, dir) {
45
46
  }
46
47
 
47
48
  class JobData {
48
- constructor(data) {
49
- if (data) this.append(data)
49
+ constructor(isTs, name, xlsxDir) {
50
+ this.#isTs = isTs
51
+ this.#name = name
52
+ this.#xlsxDir = xlsxDir
50
53
  }
51
54
 
52
55
  #data = []
56
+ #pk = null
57
+ #isTs = false
58
+ #name = ''
59
+ #xlsxDir = ''
53
60
 
54
- append(data) {
55
- this.#data.push(data)
61
+ append({ data: [data, pk], keys }) {
62
+ this.#data.push({ data, keys })
63
+ if (pk) this.#pk = pk
56
64
  }
57
65
 
58
- result() {
66
+ async result() {
59
67
  if (!this.#data.length) return {}
60
68
  let result
61
69
  for (const { keys, data } of this.#data) {
@@ -72,7 +80,15 @@ class JobData {
72
80
  }
73
81
  r[last] = this.#combine(r[last], data)
74
82
  }
75
- return result
83
+ if (!this.#isTs) return { data: result }
84
+
85
+ // TODO: 这里调用数据清洗
86
+ return await calibrateTsData(
87
+ result,
88
+ this.#name,
89
+ path.join(this.#xlsxDir, `${this.#name}.types.ts`),
90
+ this.#pk,
91
+ )
76
92
  }
77
93
 
78
94
  #combine(a, b) {
@@ -90,4 +106,8 @@ class JobData {
90
106
  for (const key in b) if (!result[key]) result[key] = b[key]
91
107
  return result
92
108
  }
109
+
110
+ get name() {
111
+ return this.#name
112
+ }
93
113
  }