config2ts 2.3.4 → 2.5.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/README.md CHANGED
@@ -8,28 +8,82 @@ run command: `npm install -g config2ts`
8
8
 
9
9
  # how to use
10
10
 
11
- config2ts -d config -o dist -m csv.ts
11
+ config2ts -d config -o dist -n csv.ts -a public
12
12
 
13
13
  # support type
14
14
 
15
- | csv field | typescript type |
16
- | :-------: | :-------------: |
17
- | Index | string |
18
- | String | string |
19
- | Number | number |
20
- | Boolean | boolean |
21
- | Enum | type |
22
- | EnumIndex | type |
23
- | String[] | string[] |
24
- | Number[] | number[] |
25
- | Enum[] | type[] |
15
+ | csv field | typescript type |
16
+ | :--------: | :---------------: |
17
+ | Index | string |
18
+ | String | string |
19
+ | Number | number |
20
+ | Boolean | boolean |
21
+ | Enum | type |
22
+ | EnumIndex | type |
23
+ | String[] | string[] |
24
+ | Number[] | number[] |
25
+ | Enum[] | type[] |
26
+ | Ref | namespace.Record |
27
+ | RefEnum | namespace.type |
28
+ | RefEnum[] | namespace.type[] |
29
+ | Object | type (Record) |
26
30
 
27
31
  * `Number` support Infinity and NaN
28
32
  * `Enum` support empty string type
29
33
  * `Enum[]` do not include empty string type
30
34
  * `EnumIndex` will generate index type, and will use `Enum` type to generate interface
35
+ * `Ref` / `RefEnum` reference another csv's Record/type by id
36
+ * `Object` parse `key:value,key:value` format, auto-infer value types (number/boolean/string), generates a dedicated `type` that merges all keys
31
37
 
32
- ## Usage
38
+ ## assets2ts
39
+
40
+ Scan assets directory and generate an `assets.ts` index file with nested resource tree.
41
+
42
+ - `-a, --assets <path>` set assets resource directory. default: `public`
43
+ - Output file: `assets.ts` (alongside the merged config file)
44
+ - Directory/filenames are converted to camelCase keys
45
+ - `type` field uses file extension (lowercase), e.g. `'png'`, `'mp3'`, `'svg'`
46
+ - Directories containing 2+ files of the same extension get a dedicated type (e.g. `PngAsset`, `Mp3Asset`) and `as Record<string, XxxAsset>` annotation
47
+ - Directories with ≤1 file or mixed extensions do not get a type annotation
48
+ - Nested directories are supported (e.g. `public/sub/image/`)
49
+
50
+ ### generated example
51
+
52
+ ```typescript
53
+ // Auto Generated by config2ts, DO NOT EDIT
54
+
55
+ export type Mp3Asset = { path: string; type: 'mp3' };
56
+ export type PngAsset = { path: string; type: 'png' };
57
+ export type SvgAsset = { path: string; type: 'svg' };
58
+
59
+ export const RES = {
60
+ public: {
61
+ image: {
62
+ adjustHorizontal: {path:'public/image/adjust-horizontal.png',type:'png'},
63
+ direction: {path:'public/image/direction.png',type:'png'}
64
+ } as Record<string, PngAsset>,
65
+ music: {
66
+ effect1: {path:'public/music/effect1.mp3',type:'mp3'},
67
+ effect2: {path:'public/music/effect2.mp3',type:'mp3'}
68
+ } as Record<string, Mp3Asset>,
69
+ singleFile: {
70
+ effect1: {path:'public/single-file/effect1.mp3',type:'mp3'}
71
+ }
72
+ }
73
+ };
74
+ ```
75
+
76
+ ### usage
77
+
78
+ ```typescript
79
+ import { RES } from "./assets";
80
+
81
+ const meta = RES.public.image.adjustHorizontal;
82
+ // meta.path → 'public/image/adjust-horizontal.png'
83
+ // meta.type → 'png'
84
+ ```
85
+
86
+ ## Options
33
87
 
34
88
  ```
35
89
  Options:
@@ -38,7 +92,14 @@ config2ts -d config -o dist -m csv.ts
38
92
  -V, --version output the version number
39
93
  -d, --dir <path> set convert path. default: ./
40
94
  -o, --outDir <path> set outDir path. default: ./
41
- -p, --prefix <prefix> set interface prefix. default: ""
42
- -s, --suffix <suffix> set interface suffix. default: "Csv"
43
- -m, --merge <name> merge all to one ts file. default: csv.ts
95
+ -n, --name <name> output file name. default: csv.ts
96
+ -a, --assets <path> set assets resource directory for asset index. default: public
44
97
  ```
98
+
99
+ ## supported file formats
100
+
101
+ | format | extension |
102
+ | :----: | :-------: |
103
+ | csv | .csv |
104
+ | ini | .ini |
105
+ | toml | .toml |
@@ -1,7 +1,8 @@
1
1
  type ConvertHandler = (str: string, moduleName: string) => string;
2
2
  export declare const Convert: Record<string, ConvertHandler>;
3
+ export declare function assets2ts(assetsDir: string): string;
3
4
  export declare function GetTsString(filePath: string): string;
4
5
  export declare function GetTsStringFromFileList(fileList: string[]): string;
5
6
  export declare function GetValidFileList(fileList: string[]): string[];
6
- export declare function startConvert(dir: string, outDir: string, merge: string): void;
7
+ export declare function startConvert(dir: string, outDir: string, merge: string, assetsDir?: string): void;
7
8
  export {};
package/lib/config2ts.js CHANGED
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.Convert = void 0;
37
+ exports.assets2ts = assets2ts;
37
38
  exports.GetTsString = GetTsString;
38
39
  exports.GetTsStringFromFileList = GetTsStringFromFileList;
39
40
  exports.GetValidFileList = GetValidFileList;
@@ -48,10 +49,12 @@ const EnumStr = "Enum";
48
49
  const EnumIndexStr = "EnumIndex";
49
50
  const EnumArrayString = "Enum[]";
50
51
  const IndexStr = "Index";
52
+ const ObjectStr = "Object";
51
53
  const RefPrefix = "Ref[";
52
54
  const RefSuffix = "]";
53
55
  const RefEnumPrefix = "RefEnum[";
54
56
  const RefEnumArraySuffix = "][]";
57
+ const AutoGenHeader = "// Auto Generated by config2ts, DO NOT EDIT\n\n";
55
58
  exports.Convert = {
56
59
  ini: function (str, moduleName) {
57
60
  const obj = toml.parse(str);
@@ -84,7 +87,10 @@ function csv2ts(csvString, moduleName) {
84
87
  }
85
88
  else {
86
89
  for (const k in d) {
87
- if (typeof global[convert[k]] === "function") {
90
+ if (convert[k] === ObjectStr) {
91
+ d[k] = parseObject(d[k]);
92
+ }
93
+ else if (typeof global[convert[k]] === "function") {
88
94
  d[k] = global[convert[k]](d[k]);
89
95
  }
90
96
  else if (convert[k] === "String[]") {
@@ -166,6 +172,24 @@ function csv2ts(csvString, moduleName) {
166
172
  template += ` export type ${field} = ${enumValueStrings.join(" | ")};\n`;
167
173
  template += ` export const ${field}List: ${field}[] = [${enumValueStrings.join(", ")}];\n\n`;
168
174
  }
175
+ else if (convert[field] == ObjectStr) {
176
+ const keyTypes = {};
177
+ for (const row of result) {
178
+ const objValue = row[field];
179
+ for (const key in objValue) {
180
+ if (!keyTypes[key]) {
181
+ keyTypes[key] = new Set();
182
+ }
183
+ keyTypes[key].add(getTypeName(objValue[key]));
184
+ }
185
+ }
186
+ template += ` export type ${field} = {\n`;
187
+ for (const key in keyTypes) {
188
+ const types = Array.from(keyTypes[key]).join(" | ");
189
+ template += ` ${serializeField(key)}?: ${types};\n`;
190
+ }
191
+ template += ` };\n\n`;
192
+ }
169
193
  }
170
194
  template += " export interface Record {\n";
171
195
  let indexField = null;
@@ -185,6 +209,9 @@ function csv2ts(csvString, moduleName) {
185
209
  else if (convert[field] == EnumArrayString) {
186
210
  fieldType = field + "[]";
187
211
  }
212
+ else if (convert[field] == ObjectStr) {
213
+ fieldType = field;
214
+ }
188
215
  else if (convert[field].startsWith(RefPrefix) && convert[field].endsWith(RefSuffix)) {
189
216
  const refModule = convert[field].slice(RefPrefix.length, -RefSuffix.length);
190
217
  const refModuleName = changeCase.pascalCase(refModule);
@@ -247,6 +274,43 @@ function serializeField(key) {
247
274
  const objString = json5.stringify(obj);
248
275
  return objString.includes("'") ? "'" + key + "'" : key;
249
276
  }
277
+ function parseObject(str) {
278
+ const result = {};
279
+ const trimmed = str.trim();
280
+ if (trimmed === "") {
281
+ return result;
282
+ }
283
+ const pairs = trimmed.split(",");
284
+ for (const pair of pairs) {
285
+ const colonIndex = pair.indexOf(":");
286
+ if (colonIndex > 0) {
287
+ const key = pair.slice(0, colonIndex).trim();
288
+ const value = pair.slice(colonIndex + 1).trim();
289
+ result[key] = parseObjectValue(value);
290
+ }
291
+ }
292
+ return result;
293
+ }
294
+ function parseObjectValue(value) {
295
+ if (value === "true")
296
+ return true;
297
+ if (value === "false")
298
+ return false;
299
+ if (value === "")
300
+ return "";
301
+ const num = Number(value);
302
+ if (!isNaN(num)) {
303
+ return num;
304
+ }
305
+ return value;
306
+ }
307
+ function getTypeName(value) {
308
+ if (typeof value === "number")
309
+ return "number";
310
+ if (typeof value === "boolean")
311
+ return "boolean";
312
+ return "string";
313
+ }
250
314
  function parseRefEnum(typeStr) {
251
315
  if (typeStr.startsWith(RefEnumPrefix)) {
252
316
  let isArray = false;
@@ -271,6 +335,123 @@ function GetFileExt(filePath) {
271
335
  const pathObject = path.parse(filePath);
272
336
  return pathObject.ext.slice(1);
273
337
  }
338
+ function scanDir(dir, basePath) {
339
+ const node = {};
340
+ let hasContent = false;
341
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
342
+ for (const entry of entries) {
343
+ const fullPath = path.join(dir, entry.name);
344
+ if (entry.isDirectory()) {
345
+ const key = changeCase.camelCase(entry.name);
346
+ const childNode = scanDir(fullPath, path.posix.join(basePath, entry.name));
347
+ if (childNode !== null) {
348
+ node[key] = childNode;
349
+ hasContent = true;
350
+ }
351
+ }
352
+ else if (entry.isFile()) {
353
+ const parsed = path.parse(entry.name);
354
+ const ext = parsed.ext.slice(1).toLowerCase();
355
+ const key = changeCase.camelCase(parsed.name);
356
+ const relPath = path.posix.join(basePath, entry.name);
357
+ node[key] = { path: relPath, type: ext };
358
+ hasContent = true;
359
+ }
360
+ }
361
+ return hasContent ? node : null;
362
+ }
363
+ function isLeafNode(value) {
364
+ return "path" in value && "type" in value;
365
+ }
366
+ function getUniformType(node) {
367
+ const values = Object.values(node);
368
+ if (values.length <= 1)
369
+ return null;
370
+ let uniformType = null;
371
+ for (const v of values) {
372
+ if (!isLeafNode(v))
373
+ return null;
374
+ if (uniformType === null) {
375
+ uniformType = v.type;
376
+ }
377
+ else if (uniformType !== v.type) {
378
+ return null;
379
+ }
380
+ }
381
+ return uniformType;
382
+ }
383
+ function collectUniformDirs(node, dirTypes, exts) {
384
+ const ut = getUniformType(node);
385
+ if (ut !== null) {
386
+ dirTypes.set(node, ut);
387
+ exts.add(ut);
388
+ return;
389
+ }
390
+ for (const v of Object.values(node)) {
391
+ if (!isLeafNode(v)) {
392
+ collectUniformDirs(v, dirTypes, exts);
393
+ }
394
+ }
395
+ }
396
+ function extToTypeName(ext) {
397
+ return ext.charAt(0).toUpperCase() + ext.slice(1) + "Asset";
398
+ }
399
+ function serializeAssetNode(node, indent, uniformDirs, extTypeMap) {
400
+ const entries = Object.entries(node);
401
+ if (entries.length === 0)
402
+ return `${indent}{}`;
403
+ const ut = uniformDirs.get(node);
404
+ const lines = [];
405
+ for (let i = 0; i < entries.length; i++) {
406
+ const [key, value] = entries[i];
407
+ const serializedKey = serializeField(key);
408
+ const comma = i < entries.length - 1 ? "," : "";
409
+ if (isLeafNode(value)) {
410
+ lines.push(`${indent}${serializedKey}: {path:${json5.stringify(value.path)},type:${json5.stringify(value.type)}}${comma}`);
411
+ }
412
+ else {
413
+ const childUt = uniformDirs.get(value);
414
+ const typeAnnotation = childUt ? ` as Record<string, ${extTypeMap.get(childUt)}>` : "";
415
+ lines.push(`${indent}${serializedKey}: {`);
416
+ lines.push(serializeAssetNode(value, indent + " ", uniformDirs, extTypeMap));
417
+ lines.push(`${indent}}${typeAnnotation}${comma}`);
418
+ }
419
+ }
420
+ return lines.join("\n");
421
+ }
422
+ function assets2ts(assetsDir) {
423
+ if (!fs.existsSync(assetsDir)) {
424
+ return "";
425
+ }
426
+ const dirName = path.basename(assetsDir);
427
+ const root = scanDir(assetsDir, dirName);
428
+ if (root === null) {
429
+ return "";
430
+ }
431
+ const uniformDirs = new Map();
432
+ const exts = new Set();
433
+ collectUniformDirs(root, uniformDirs, exts);
434
+ const extTypeMap = new Map();
435
+ const sortedExts = Array.from(exts).sort();
436
+ for (const ext of sortedExts) {
437
+ extTypeMap.set(ext, extToTypeName(ext));
438
+ }
439
+ let template = "";
440
+ for (const ext of sortedExts) {
441
+ const typeName = extTypeMap.get(ext);
442
+ template += `export type ${typeName} = { path: string; type: ${json5.stringify(ext)} };\n`;
443
+ }
444
+ if (sortedExts.length > 0)
445
+ template += "\n";
446
+ template += `export const RES = {\n`;
447
+ const rootUt = uniformDirs.get(root);
448
+ template += ` ${dirName}: {\n`;
449
+ template += serializeAssetNode(root, " ", uniformDirs, extTypeMap);
450
+ const rootAnnotation = rootUt ? ` as Record<string, ${extTypeMap.get(rootUt)}>` : "";
451
+ template += `\n }${rootAnnotation}\n`;
452
+ template += `};\n`;
453
+ return template;
454
+ }
274
455
  function GetTsString(filePath) {
275
456
  const pathObject = path.parse(filePath);
276
457
  const handle = exports.Convert[GetFileExt(filePath)];
@@ -297,12 +478,20 @@ function GetValidFileList(fileList) {
297
478
  return exports.Convert[GetFileExt(filePath)] != null;
298
479
  });
299
480
  }
300
- function startConvert(dir, outDir, merge) {
481
+ function startConvert(dir, outDir, merge, assetsDir) {
301
482
  const fileList = fs.readdirSync(dir);
302
483
  const fullFileList = GetValidFileList(fileList).map(function (filename) {
303
484
  return path.join(dir, filename);
304
485
  });
305
486
  const mergeFile = path.join(outDir, merge);
306
- fs.writeFileSync(mergeFile, GetTsStringFromFileList(fullFileList), { encoding: "utf-8" });
487
+ fs.writeFileSync(mergeFile, AutoGenHeader + GetTsStringFromFileList(fullFileList), { encoding: "utf-8" });
307
488
  console.log(`config2ts, ${fullFileList.length} config files, merge into: ${mergeFile}`);
489
+ if (assetsDir) {
490
+ const assetsOutput = assets2ts(assetsDir);
491
+ if (assetsOutput) {
492
+ const assetsFile = path.join(outDir, "assets.ts");
493
+ fs.writeFileSync(assetsFile, AutoGenHeader + assetsOutput, { encoding: "utf-8" });
494
+ console.log(`assets2ts, resource dir: ${assetsDir}, output: ${assetsFile}`);
495
+ }
496
+ }
308
497
  }
package/lib/main.js CHANGED
@@ -45,6 +45,7 @@ program
45
45
  .option("-n, --name <name>", "output file name", "csv.ts")
46
46
  .option("-d, --dir <path>", "set convert path", ".")
47
47
  .option("-o, --outDir <path>", "set outDir path")
48
+ .option("-a, --assets <path>", "set assets resource directory for asset index", "public")
48
49
  .parse(process.argv);
49
50
  const options = program.opts();
50
51
  console.log(options);
@@ -55,4 +56,8 @@ console.log("outDir:", outDir);
55
56
  if (!fs.existsSync(outDir)) {
56
57
  fs.mkdirSync(outDir, { recursive: true });
57
58
  }
58
- (0, config2ts_1.startConvert)(dir, outDir, options.name);
59
+ const assetsDir = options.assets ? path.resolve(options.assets) : undefined;
60
+ if (assetsDir) {
61
+ console.log("assetsDir:", assetsDir);
62
+ }
63
+ (0, config2ts_1.startConvert)(dir, outDir, options.name, assetsDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "config2ts",
3
- "version": "2.3.4",
3
+ "version": "2.5.0",
4
4
  "description": "convert config to ts file.",
5
5
  "main": "lib/config2ts.js",
6
6
  "types": "lib/config2ts.d.ts",