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 +77 -16
- package/lib/config2ts.d.ts +2 -1
- package/lib/config2ts.js +192 -3
- package/lib/main.js +6 -1
- package/package.json +1 -1
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 -
|
|
11
|
+
config2ts -d config -o dist -n csv.ts -a public
|
|
12
12
|
|
|
13
13
|
# support type
|
|
14
14
|
|
|
15
|
-
| csv field
|
|
16
|
-
|
|
|
17
|
-
| Index
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
| Boolean
|
|
21
|
-
|
|
|
22
|
-
| EnumIndex
|
|
23
|
-
| String[]
|
|
24
|
-
| Number[]
|
|
25
|
-
| Enum[]
|
|
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
|
-
##
|
|
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
|
-
-
|
|
42
|
-
-
|
|
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 |
|
package/lib/config2ts.d.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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);
|