v-transform 2.3.2 → 3.0.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,7 +4,7 @@
4
4
  "vt": "src/index.js"
5
5
  },
6
6
  "type": "module",
7
- "version": "2.3.2",
7
+ "version": "3.0.0",
8
8
  "main": "index.js",
9
9
  "scripts": {
10
10
  "clean": "npx rimraf test",
package/src/calibrator.js CHANGED
@@ -1,27 +1,21 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { generate } from 'ts-to-zod'
4
+ import { z } from 'zod'
4
5
 
5
- export async function calibrateTsData(data, name, types, pk) {
6
- if (!types || !fs.existsSync(types)) {
7
- console.warn(
8
- `⚠️ 提示: Excel 同级目录下未找到 [${name}.types.ts],将退回普通无类型导出。`,
9
- )
10
- const isArray = Array.isArray(data)
11
- const extra = {
12
- typeSign: isArray ? 'any[]' : 'Map<any, any>',
13
- types: isArray ? `export type ${name.toUpperCase()} = any;` : '',
14
- isNativeMap: !isArray,
15
- }
16
- return { data, extra }
6
+ export async function calibrateTsData(raw, name, dts) {
7
+ if (!dts || !fs.existsSync(dts)) {
8
+ console.warn(`⚠️ [Calibrator] ${name}.types.ts`)
9
+ return { raw, type: 'any', dts: '' }
17
10
  }
18
11
 
19
- console.info(
20
- `🔍 [vTransform TS] 成功在 Excel 目录定位规范,正在校准 [${name}]...`,
21
- )
22
- const sourceText = fs.readFileSync(types, 'utf-8')
23
- const options = { keepOptionalProperties: true }
24
- const zod = generate({ sourceText, options })
12
+ console.info(`🔍 [Calibrator] ${name}.types.ts`)
13
+ const sourceText = fs.readFileSync(dts, 'utf-8')
14
+ const options = {
15
+ keepOptionalProperties: true,
16
+ getSchemaName: identifier => `${identifier}Schema`,
17
+ }
18
+ const zod = generate({ sourceText, ...options })
25
19
  const tsFileBase64 = Buffer.from(sourceText).toString('base64')
26
20
  const configModule = await import(
27
21
  `data:text/javascript;base64,${tsFileBase64}`
@@ -54,114 +48,74 @@ export async function calibrateTsData(data, name, types, pk) {
54
48
  /z\.boolean\(\)/g,
55
49
  `z.preprocess(globalThis.__MERGED_TRANSFORMERS__.boolean, z.boolean())`,
56
50
  )
57
- Object.keys(customTransformers).forEach(fieldKey => {
58
- if (fieldKey === 'string' || fieldKey === 'boolean') return
59
-
60
- const targetRegex = new RegExp(
61
- `(${fieldKey}:\\s*)(z\\.[a-zA-Z_0-9]+(?:\\([^)]*\\))?)`,
62
- 'g',
63
- )
64
- zodRawCode = zodRawCode.replace(
65
- targetRegex,
66
- (m, prefix, originalZodType) => {
67
- mergedTransformers[fieldKey] = customTransformers[fieldKey]
68
- return `${prefix}z.preprocess(globalThis.__MERGED_TRANSFORMERS__.${fieldKey}, ${originalZodType})`
69
- },
70
- )
71
- })
72
51
 
73
52
  globalThis.__MERGED_TRANSFORMERS__ = mergedTransformers
74
53
  const base64Code = Buffer.from(zodRawCode).toString('base64')
75
54
  const zodModule = await import(`data:text/javascript;base64,${base64Code}`)
76
- delete globalThis.__MERGED_TRANSFORMERS__
77
- const targetSchemaName = `${name}Schema`
78
- const RowZodValidator = zodModule[targetSchemaName]
79
55
 
56
+ const search = `${name.replace(/[-_]/g, '')}Schema`.toLocaleLowerCase()
57
+ const targetSchemaName = Object.keys(zodModule).find(key => {
58
+ if (key.toLocaleLowerCase() === search) {
59
+ console.info(`🎯 [Calibrator] ${name} -> ${key}`)
60
+ return true
61
+ }
62
+ })
63
+ const RowZodValidator = zodModule[targetSchemaName]
80
64
  if (!RowZodValidator) {
81
- throw new Error(
82
- `无法从类型文件中自动推导出名为 ${targetSchemaName} 的主校验器。`,
83
- )
65
+ delete globalThis.__MERGED_TRANSFORMERS__
66
+ throw new Error(`❌ [Calibrator] ${targetSchemaName} Missmatch`)
84
67
  }
68
+ const typeName = targetSchemaName.replace(/Schema$/, '')
69
+ dts = sourceText.split(/.*@vt-types-end.*/i)[0]
70
+ const data = { raw, dts, type: typeName }
71
+ Object.keys(customTransformers).forEach(fieldKey => {
72
+ if (fieldKey === 'string' || fieldKey === 'boolean') return
73
+ if (RowZodValidator.shape && RowZodValidator.shape[fieldKey]) {
74
+ const originalValidator = RowZodValidator.shape[fieldKey]
75
+ RowZodValidator.shape[fieldKey] = z.preprocess(
76
+ customTransformers[fieldKey],
77
+ originalValidator,
78
+ )
79
+ mergedTransformers[fieldKey] = customTransformers[fieldKey]
80
+ }
81
+ })
85
82
 
86
- let cleanData
87
- let typeSign = ''
88
- const typeName = name.charAt(0).toUpperCase() + name.slice(1)
89
-
90
- if (Array.isArray(data)) {
91
- cleanData = []
92
- typeSign = `${typeName}[]`
93
- for (let i = 0; i < data.length; i++) {
94
- const parseResult = RowZodValidator.safeParse(data[i])
83
+ globalThis.__MERGED_TRANSFORMERS__ = mergedTransformers
84
+ if (Array.isArray(raw)) {
85
+ for (let i = 0; i < raw.length; i++) {
86
+ const parseResult = RowZodValidator.safeParse(raw[i])
95
87
  if (!parseResult.success) {
96
- printZodError(
97
- name,
98
- `第 ${i + 1} 条数据`,
99
- parseResult.error.issues,
100
- data[i],
101
- )
88
+ delete globalThis.__MERGED_TRANSFORMERS__
89
+ printZodError(name, i, parseResult.error.issues, raw[i])
102
90
  }
103
- cleanData.push(parseResult.data)
91
+ raw[i] = parseResult.data
104
92
  }
105
- console.info(`📦 [Array 模式] 完美对齐数组规范`)
106
- } else if (typeof data === 'object' && data !== null) {
107
- const tempCleanMap = {}
108
- for (const key in data) {
109
- const parseResult = RowZodValidator.safeParse(data[key])
93
+ console.info(`✅ [Calibrator] array`)
94
+ } else if (typeof raw === 'object' && raw !== null) {
95
+ for (const key in raw) {
96
+ const parseResult = RowZodValidator.safeParse(raw[key])
110
97
  if (!parseResult.success) {
111
- printZodError(
112
- name,
113
- `Key 为 [${key}] 的数据`,
114
- parseResult.error.issues,
115
- data[key],
116
- )
98
+ delete globalThis.__MERGED_TRANSFORMERS__
99
+ printZodError(name, key, parseResult.error.issues, raw[key])
117
100
  }
118
- tempCleanMap[key] = parseResult.data
101
+ raw[key] = parseResult.data
119
102
  }
120
- let keyType = 'string'
121
- if (pk && RowZodValidator.shape && RowZodValidator.shape[pk]) {
122
- const validator = RowZodValidator.shape[pk]
123
- if (
124
- validator.safeParse(1).success &&
125
- !validator.safeParse('1').success
126
- ) {
127
- keyType = 'number'
128
- }
129
- }
130
-
131
- const isNumKey = keyType === 'number'
132
- cleanData = Object.entries(tempCleanMap).map(([k, v]) => [
133
- isNumKey ? Number(k) : k,
134
- v,
135
- ])
136
- typeSign = `Map<${keyType}, ${typeName}>`
137
- console.info(
138
- `🎯 [Map 模式] 完美对齐原生 Map 规范,主键 [${pk}] 自动识别为: ${keyType}`,
139
- )
103
+ console.info(`✅ [Calibrator] object`)
140
104
  }
141
105
 
142
- const extra = {
143
- typeSign,
144
- types: sourceText,
145
- isNativeMap: !Array.isArray(data),
146
- }
147
- return { data: cleanData, extra }
106
+ delete globalThis.__MERGED_TRANSFORMERS__
107
+ return data
148
108
  }
149
109
 
150
- function printZodError(sheetName, positionLabel, issues, rawRowData) {
151
- console.error(`\n❌ [vTransform 数据校准失败]`)
152
- console.error(
153
- `👉 发生位置: 表格 [${sheetName}] ${positionLabel}不兼容你的 TS 类型限制!`,
154
- )
155
- console.error(`👉 冲突细节详情:`)
110
+ function printZodError(sheetName, key, issues, rawRowData) {
111
+ console.error(`❌ [Calibrator] ${sheetName}\x1b[33m[${key}]\x1b[0m`)
156
112
  issues.forEach(err => {
157
- const wrongField = err.path.join('.') || '根节点'
158
- console.error(` - 错误字段: \x1b[33m${wrongField}\x1b[0m`)
159
- console.error(` - 期待类型/约束: ${err.message}`)
113
+ const wrongField = err.path.join('.') || 'root'
160
114
  console.error(
161
- ` - 当前收到的脏数据值:`,
162
- rawRowData[wrongField] ?? '缺失字段',
115
+ ` - \x1b[33m${wrongField}\x1b[0m:`,
116
+ rawRowData[wrongField],
163
117
  )
164
- console.error(` -----------------------------------------`)
118
+ console.error(` - ${err.message}`)
165
119
  })
166
120
  process.exit(1)
167
121
  }
package/src/dump.js CHANGED
@@ -6,43 +6,103 @@ function j(data, space) {
6
6
  return JSON.stringify(data, null, space)
7
7
  }
8
8
 
9
- function jsonify(name, { data, addition }, space) {
10
- return j(addition ? { [name]: data, ...addition } : data, space)
9
+ function minij(json) {
10
+ return json.replace(/"([a-zA-Z_$][a-zA-Z0-9_$]*)"\s*:/g, '$1:')
11
11
  }
12
12
 
13
- function cjsify(name, data, space) {
14
- return `module.exports = ${jsonify(name, data, space)}`
13
+ function jp(data, space) {
14
+ const json = j(data, space).replace(/\\/g, '\\\\').replace(/'/g, "\\'")
15
+ return `JSON.parse(\n\t'${json}'\n)`
15
16
  }
16
17
 
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}`
18
+ function map(raw, pk, space) {
19
+ const isArray = !pk || Array.isArray(raw)
20
+ if (isArray || typeof raw !== 'object') {
21
+ const data = jp(j(raw, space))
22
+ return { data: jp(j(raw, space)), type: isArray ? 'array' : 'object' }
23
+ }
24
+ const data = Object.values(raw).map(v => [v[pk], v])
25
+ console.info(`🔁 [Dump] Object -> Map [${pk}]`)
26
+ return { data: `new Map(${jp(j(data, space))})`, type: 'map' }
27
+ }
28
+
29
+ function withType(main, sub, pk) {
30
+ switch (main) {
31
+ case 'array':
32
+ return `${sub}[]`
33
+ case 'map':
34
+ if (sub === 'any') return `Map<${sub}, ${sub}>`
35
+ return `Map<${sub}['${pk}'], ${sub}>`
36
+ case 'object':
37
+ return `{ [key: string]: ${sub} }`
38
+ default:
39
+ return sub
40
+ }
25
41
  }
26
42
 
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)
43
+ function jsonify(name, data, space) {
44
+ const { raw, addition, dts } = data
45
+ const json = j(addition ? { [name]: raw, ...addition } : raw, space)
46
+ return [json, dts]
31
47
  }
32
48
 
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}`
49
+ function yamlify(name, data, indent) {
50
+ const { raw, addition, dts } = data
51
+ const opt = { indent: indent || undefined }
52
+ const yaml = dumpYAML(addition ? { [name]: raw, ...addition } : raw, opt)
53
+ return [yaml, dts]
54
+ }
55
+
56
+ function cjsify(name, data, space, types) {
57
+ const { type, pk, dts, addition } = data
58
+ const converted = map(data.raw, pk, space)
59
+ const r = []
60
+ if (types && dts) {
61
+ r.push(`/** @typedef {import('./${name}').${type}} ${type} */`)
62
+ r.push(`/** @type {${withType(converted.type, type, pk)}} */`)
63
+ }
64
+ r.push(`const ${name} = ${converted.data}`)
65
+ const exports = [name]
66
+ if (addition)
67
+ Object.entries(addition).forEach(([key, value]) => {
68
+ exports.push(key)
69
+ r.push(`const ${key} = ${j(value, space)}`)
70
+ })
71
+
72
+ r.push(`module.exports = { ${exports.join(', ')} }`)
73
+ return [r.join('\n'), dts]
74
+ }
75
+
76
+ function esmify(name, data, space, types) {
77
+ const { type, pk, dts, addition } = data
78
+ const converted = map(data.raw, pk, space)
79
+ const rows = []
80
+ if (types && dts) {
81
+ rows.push(`/** @typedef {import('./${name}').${type}} ${type} */`)
82
+ rows.push(`/** @type {${withType(converted.type, type, pk)}} */`)
83
+ }
84
+ rows.push(`export const ${name} = ${converted.data}`)
85
+ if (addition)
86
+ Object.entries(addition).forEach(([key, value]) => {
87
+ rows.push(`export const ${key} = ${j(value, space)}`)
88
+ })
89
+
90
+ rows.push(`export default ${name}`)
91
+ return [rows.join('\n'), dts]
92
+ }
93
+
94
+ function tsify(name, data, space) {
95
+ const { pk, addition } = data
96
+ const converted = map(data.raw, pk, space)
97
+ const type = withType(converted.type, data.type, pk)
98
+ const rows = [data.dts]
99
+ rows.push(`export const ${name} = ${converted.data} as unknown as ${type};`)
100
+ if (addition)
101
+ Object.entries(addition).forEach(([key, value]) =>
102
+ rows.push(`export const ${key} = ${j(value, space)}`),
103
+ )
104
+ rows.push(`export default ${name}`)
105
+ return [rows.join('\n')]
46
106
  }
47
107
 
48
108
  async function mkdirs(dir) {
@@ -58,38 +118,63 @@ async function mkdirs(dir) {
58
118
  }
59
119
 
60
120
  async function write(sheet, data) {
61
- console.info(`Dump ${sheet}`)
121
+ console.info(`📦 -> ${sheet}`)
62
122
  await mkdirs(path.dirname(sheet))
63
123
  await writeFile(sheet, data)
64
124
  }
65
125
 
66
- export async function dump(sheet, data, type, space, name) {
67
- let ext, ify
126
+ export async function dump({ sheet, data, type, space, name, ext, types }) {
127
+ let e, ify
68
128
  switch (type) {
69
129
  case 'ts':
70
- ext = '.ts'
130
+ e = '.ts'
71
131
  ify = tsify
72
132
  break
73
133
  case 'cjs':
74
- ext = '.js'
134
+ e = '.js'
75
135
  ify = cjsify
76
136
  break
77
137
  case 'js':
78
138
  case 'mjs':
79
139
  case 'esm':
80
- ext = '.js'
140
+ e = '.js'
81
141
  ify = esmify
82
142
  break
83
143
  case 'yaml':
84
144
  case 'yml':
85
- ext = '.yaml'
145
+ e = '.yaml'
86
146
  ify = yamlify
87
147
  break
88
148
  case 'json':
89
149
  default:
90
- ext = '.json'
150
+ e = '.json'
91
151
  ify = jsonify
92
152
  break
93
153
  }
94
- return write(`${sheet}${ext}`, ify(name, data, space))
154
+
155
+ const [main, dts] = ify(name, data, space, types)
156
+ const result = await write(`${sheet}${ext || e}`, main)
157
+ if (types && dts) await write(`${sheet}.d.ts`, dts)
158
+
159
+ // if (type !== 'ts' && data?.extra?.types) {
160
+ // const { types, typeSign } = data.extra
161
+ // const cleanTypes = String(types).trim() + '\n'
162
+
163
+ // const dtsLines = [
164
+ // cleanTypes,
165
+ // `export declare const ${name}: ${typeSign};`,
166
+ // `export default ${name};`,
167
+ // ]
168
+
169
+ // if (data.addition) {
170
+ // Object.entries(data.addition).forEach(([key, value]) => {
171
+ // dtsLines.push(`export declare const ${key}: ${typeof value};`)
172
+ // })
173
+ // }
174
+
175
+ // const dtsContent = dtsLines.join('\n') + '\n'
176
+ // await write(`${sheet}.d.ts`, dtsContent)
177
+ // }
178
+
179
+ return result
95
180
  }
package/src/index.js CHANGED
@@ -19,7 +19,9 @@ program
19
19
  .option('-w, --cwd <cwd>', 'current work dir', null)
20
20
  .option('-o, --output <output>', 'output dir', null)
21
21
  .option('-d, --dest <dest>', 'dest dir', null)
22
+ .option('-e, --ext <ext>', 'file ext', null)
22
23
  .option('-a, --addition', 'addition version', false)
24
+ .option('-x, --notypes', 'disable types generation', false)
23
25
  .action((list, options) => {
24
26
  if (!options.dest) options.dest = options.output
25
27
  options.list = list
package/src/loader.js CHANGED
@@ -3,13 +3,15 @@ import { load as loadYAML } from 'js-yaml'
3
3
  import path from 'node:path'
4
4
  import { readFile } from 'node:fs/promises'
5
5
 
6
- export async function load({ type, space, config, dest, list, cwd, addition }) {
6
+ export async function load(args) {
7
+ const { config, list, addition, ext } = args
7
8
  const version = new Date().toISOString()
8
- type = type || 'json'
9
- cwd = cwd || process.cwd()
10
- space = Number(space) || 0
11
- dest = dest || cwd
12
- const def = { cwd, type, space, dest, addition }
9
+ const type = args.type || 'json'
10
+ const cwd = args.cwd || process.cwd()
11
+ const space = Number(args.space) || 0
12
+ const dest = args.dest || cwd
13
+ const types = !args.notypes
14
+ const def = { cwd, type, space, dest, addition, ext, types }
13
15
  const m = (...l) => globit(Object.assign({}, ...l))
14
16
  const cfgs = []
15
17
 
package/src/transform.js CHANGED
@@ -12,26 +12,28 @@ export async function transform(options) {
12
12
  console.info(`Transformed in ${Date.now() - now}ms`)
13
13
  }
14
14
 
15
- async function task({ files, dest, cwd, type, space, addition }) {
16
- console.info('Transform task config:', { files, dest, cwd, type, space })
15
+ async function task(options) {
16
+ const { files, dest, cwd, type, space, addition, ext, types } = options
17
+ console.info('Transform task config:', options)
17
18
  const m = new Map()
18
19
  for (const file of files) {
19
20
  const dir = path.resolve(dest, path.dirname(file))
20
- await processPrepared(path.resolve(cwd, file), dir, m, type == 'ts')
21
+ await processPrepared(path.resolve(cwd, file), dir, m)
21
22
  }
22
23
  for (const [sheet, job] of m) {
23
- const data = await job.result()
24
- await dump(sheet, { ...data, addition }, type, space, job.name)
24
+ const data = await job.result(addition)
25
+ const d = { sheet, data, type, space, name: job.name, ext, types }
26
+ await dump(d)
25
27
  }
26
28
  }
27
29
 
28
- async function processPrepared(src, dir, m, isTs) {
30
+ async function processPrepared(src, dir, m) {
29
31
  const prepared = await prepare(src)
30
32
  const xlsxDir = path.dirname(src)
31
33
  for (const { name, data } of prepared) {
32
34
  if (name.startsWith('#')) continue
33
35
  const { sheet, keys, name: parsedName } = parseSheetAndKeys(name, dir)
34
- if (!m.has(sheet)) m.set(sheet, new JobData(isTs, parsedName, xlsxDir))
36
+ if (!m.has(sheet)) m.set(sheet, new JobData(parsedName, xlsxDir))
35
37
  m.get(sheet).append({ keys, data: parser(data) })
36
38
  }
37
39
  }
@@ -47,15 +49,13 @@ function parseSheetAndKeys(name, dir) {
47
49
  }
48
50
 
49
51
  class JobData {
50
- constructor(isTs, name, xlsxDir) {
51
- this.#isTs = isTs
52
+ constructor(name, xlsxDir) {
52
53
  this.#name = name
53
54
  this.#xlsxDir = xlsxDir
54
55
  }
55
56
 
56
57
  #data = []
57
58
  #pk = null
58
- #isTs = false
59
59
  #name = ''
60
60
  #xlsxDir = ''
61
61
 
@@ -64,8 +64,8 @@ class JobData {
64
64
  if (pk) this.#pk = pk
65
65
  }
66
66
 
67
- async result() {
68
- if (!this.#data.length) return {}
67
+ async result(addition) {
68
+ if (!this.#data.length) return { data: {}, extra: null }
69
69
  let result
70
70
  for (const { keys, data } of this.#data) {
71
71
  if (!keys.length) {
@@ -81,15 +81,9 @@ class JobData {
81
81
  }
82
82
  r[last] = this.#combine(r[last], data)
83
83
  }
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
- )
84
+ const dts = path.join(this.#xlsxDir, `${this.#name}.types.ts`)
85
+ const data = await calibrateTsData(result, this.#name, dts)
86
+ return { ...data, pk: this.#pk, addition }
93
87
  }
94
88
 
95
89
  #combine(a, b) {