config2ts 2.5.9 → 2.6.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/README.md +4 -1
- package/lib/config2ts.js +168 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,9 @@ config2ts -d config -o dist -n csv.ts -a public
|
|
|
25
25
|
| Enum[] | type[] |
|
|
26
26
|
| Ref | namespace.Record |
|
|
27
27
|
| RefEnum | namespace.type |
|
|
28
|
-
|
|
|
28
|
+
| RefEnum[] | namespace.type[] |
|
|
29
|
+
| Template | namespace.Record |
|
|
30
|
+
| Template[] | namespace.Record[] |
|
|
29
31
|
| Object | type (Record) |
|
|
30
32
|
| Object[] | type[] (Record[]) |
|
|
31
33
|
|
|
@@ -34,6 +36,7 @@ config2ts -d config -o dist -n csv.ts -a public
|
|
|
34
36
|
* `Enum[]` union contains every value actually present in the data (including `""` for empty slots)
|
|
35
37
|
* `EnumIndex` will generate index type, and will use `Enum` type to generate interface
|
|
36
38
|
* `Ref` / `RefEnum` reference another csv's Record/enum by id: `Ref[file]` is one row (`file.Map["id"]`), `Ref[file][]` is an array of rows with comma-separated ids (`[file.Map["1"],file.Map["2"]]`, typed `File.Record[]`); use `Object[]` for inline private structures and `Ref[]` for shared, identity-bearing rows
|
|
39
|
+
* `Template[file]` inherits a whole row from another table and overrides selected fields: a cell is `<baseId>|<key>:<value>,<key>:<value>` — a pipe separates the base row id from override pairs (e.g. `101|damage:150,range:8`), emitting `{ ...file.Map["101"], damage: 150, range: 8 }` typed `File.Record`; with no overrides the cell is just the id and emits a plain `file.Map["id"]` like `Ref`. Override values may be bare scalars or bracket-wrapped composites mirroring the generated TS syntax — arrays as `[v1,v2]` (e.g. `Params:[6,2.07]`, required for array-typed fields; a bare `Params:6,2.07` gets chopped by the pair comma and prints a warning) and flat objects as `{k:v,k:v}`. `Template[file][]` separates entries with `;` (the same element separator as `Object[]`), e.g. `101|damage:150;102|heal:500` emits `[{ ...file.Map["101"], damage: 150 },{ ...file.Map["102"], heal: 500 }]`. Override key typos and value-type mismatches fail at tsc compile time; overrides are flat top-level fields (no dot-path deep merge), and the target table must have an Index/EnumIndex Map (same file-ordering rule as `Ref`)
|
|
37
40
|
* `Object` parse `key:value,key:value` format, auto-infer value types (number/boolean/string), generates a dedicated `type` that merges all keys
|
|
38
41
|
* `Object[]` separates objects with `;` while commas still separate `key:value` pairs inside each object (same syntax as the `Object` type), e.g. `num:1,,str:a;num:2` makes `[{num:1,str:'a'},{num:2}]`, a bare `;` makes `[{},{}]`; generates a dedicated element `type` merging all keys with the field typed as `type[]`
|
|
39
42
|
* Array convention: primitive arrays (`String[]`/`Number[]`/`Enum[]`/`RefEnum[...] []`) separate elements with `,` and `Object[]` separates objects with `;`; raw cell values are normalized (line breaks removed, whitespace trimmed); an empty cell generates `[]`; otherwise n separators make n+1 slots and every slot is kept (including interior or trailing ones) so parallel arrays stay index-aligned. Empty slots use the type's own zero value — `''` for string/enum/ref-enum, `0` for number, `{}` for empty object slots; `null` is never generated
|
package/lib/config2ts.js
CHANGED
|
@@ -55,6 +55,12 @@ const RefPrefix = "Ref[";
|
|
|
55
55
|
const RefSuffix = "]";
|
|
56
56
|
const RefEnumPrefix = "RefEnum[";
|
|
57
57
|
const RefEnumArraySuffix = "][]";
|
|
58
|
+
const TemplatePrefix = "Template[";
|
|
59
|
+
// Inside a Template cell, '|' separates the referenced base row id from the
|
|
60
|
+
// Object-style override pairs; ';' separates multiple entries in the array
|
|
61
|
+
// form (same element separator as Object[]).
|
|
62
|
+
const TemplateOverrideSeparator = "|";
|
|
63
|
+
const TemplateEntrySeparator = ";";
|
|
58
64
|
const AutoGenHeader = "// Auto Generated by config2ts, DO NOT EDIT\n\n";
|
|
59
65
|
exports.Convert = {
|
|
60
66
|
ini: function (str, moduleName) {
|
|
@@ -131,6 +137,29 @@ function csv2ts(csvString, moduleName) {
|
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
139
|
}
|
|
140
|
+
else if (parseTemplate(convert[k])) {
|
|
141
|
+
const template = parseTemplate(convert[k]);
|
|
142
|
+
const templateContext = { moduleName, field: k, row: i };
|
|
143
|
+
if (template.isArray) {
|
|
144
|
+
const entries = d[k] === "" ? [] : d[k].split(TemplateEntrySeparator).map((part) => parseTemplateEntry(part, templateContext));
|
|
145
|
+
for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
|
|
146
|
+
if (entries[entryIndex].baseId === "") {
|
|
147
|
+
console.warn(`[config2ts] warning: ${moduleName} row ${i} field "${k}" template array entry ${entryIndex + 1} has an empty base id`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
d[k] = entries;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
if (d[k] === "") {
|
|
154
|
+
console.warn(`[config2ts] warning: ${moduleName} row ${i} field "${k}" template value is empty`);
|
|
155
|
+
}
|
|
156
|
+
const entry = parseTemplateEntry(d[k], templateContext);
|
|
157
|
+
if (d[k] !== "" && entry.baseId === "") {
|
|
158
|
+
console.warn(`[config2ts] warning: ${moduleName} row ${i} field "${k}" template base id is empty`);
|
|
159
|
+
}
|
|
160
|
+
d[k] = entry;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
134
163
|
else {
|
|
135
164
|
d[k] = String(d[k]);
|
|
136
165
|
}
|
|
@@ -226,6 +255,11 @@ function csv2ts(csvString, moduleName) {
|
|
|
226
255
|
const refModuleName = changeCase.pascalCase(refEnum.refModule);
|
|
227
256
|
fieldType = refEnum.isArray ? `${refModuleName}.${refEnum.refField}[]` : `${refModuleName}.${refEnum.refField}`;
|
|
228
257
|
}
|
|
258
|
+
else if (parseTemplate(convert[field])) {
|
|
259
|
+
const template = parseTemplate(convert[field]);
|
|
260
|
+
const refModuleName = changeCase.pascalCase(template.refModule);
|
|
261
|
+
fieldType = template.isArray ? `${refModuleName}.Record[]` : `${refModuleName}.Record`;
|
|
262
|
+
}
|
|
229
263
|
else {
|
|
230
264
|
fieldType = convert[field].toLowerCase();
|
|
231
265
|
}
|
|
@@ -260,6 +294,17 @@ function csv2ts(csvString, moduleName) {
|
|
|
260
294
|
template += ` ${serializeField(field)}: ${refModuleName}.Map[${JSON.stringify(value)}],\n`;
|
|
261
295
|
}
|
|
262
296
|
}
|
|
297
|
+
else if (parseTemplate(convert[field])) {
|
|
298
|
+
const templateType = parseTemplate(convert[field]);
|
|
299
|
+
const refModuleName = changeCase.pascalCase(templateType.refModule);
|
|
300
|
+
if (templateType.isArray) {
|
|
301
|
+
const entries = value.map((entry) => serializeTemplateEntry(entry, refModuleName)).join(",");
|
|
302
|
+
template += ` ${serializeField(field)}: [${entries}],\n`;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
template += ` ${serializeField(field)}: ${serializeTemplateEntry(value, refModuleName)},\n`;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
263
308
|
else {
|
|
264
309
|
template += ` ${serializeField(field)}: ${json5.stringify(value)},\n`;
|
|
265
310
|
}
|
|
@@ -364,6 +409,9 @@ function isKnownType(typeStr) {
|
|
|
364
409
|
if (parseRefEnum(typeStr)) {
|
|
365
410
|
return true;
|
|
366
411
|
}
|
|
412
|
+
if (parseTemplate(typeStr)) {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
367
415
|
if (typeof global[typeStr] === "function") {
|
|
368
416
|
return true;
|
|
369
417
|
}
|
|
@@ -409,6 +457,126 @@ function parseRefEnum(typeStr) {
|
|
|
409
457
|
}
|
|
410
458
|
return null;
|
|
411
459
|
}
|
|
460
|
+
// Parse Template[module] (single) and Template[module][] (array of templated
|
|
461
|
+
// row references). "Template[" starts with 'T', so it never collides with the
|
|
462
|
+
// "Ref[" / "RefEnum[" prefixes.
|
|
463
|
+
function parseTemplate(typeStr) {
|
|
464
|
+
if (!typeStr.startsWith(TemplatePrefix)) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
let isArray = false;
|
|
468
|
+
let inner = typeStr;
|
|
469
|
+
if (typeStr.endsWith(RefEnumArraySuffix)) {
|
|
470
|
+
isArray = true;
|
|
471
|
+
inner = typeStr.slice(0, -RefEnumArraySuffix.length) + RefSuffix;
|
|
472
|
+
}
|
|
473
|
+
if (inner.startsWith(TemplatePrefix) && inner.endsWith(RefSuffix)) {
|
|
474
|
+
const refModule = inner.slice(TemplatePrefix.length, -RefSuffix.length);
|
|
475
|
+
if (refModule !== "") {
|
|
476
|
+
return { refModule, isArray };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
// Parse one Template cell entry: "<baseId>|<key>:<value>,<key>:<value>".
|
|
482
|
+
// The part before the first '|' is the base row id; the part after it holds
|
|
483
|
+
// the override pairs. Pair splitting is bracket-aware, so composite values
|
|
484
|
+
// are not chopped apart: array values are wrapped in [...] (e.g.
|
|
485
|
+
// "Params:[6,2.07]") and flat object values in {...} (e.g.
|
|
486
|
+
// "params:{damage:100,range:5}"), mirroring the generated TypeScript syntax.
|
|
487
|
+
// No '|' means zero overrides, which is equivalent to a plain Ref.
|
|
488
|
+
function parseTemplateEntry(str, context) {
|
|
489
|
+
const separatorIndex = str.indexOf(TemplateOverrideSeparator);
|
|
490
|
+
if (separatorIndex < 0) {
|
|
491
|
+
return { baseId: str.trim(), overrides: {} };
|
|
492
|
+
}
|
|
493
|
+
const baseId = str.slice(0, separatorIndex).trim();
|
|
494
|
+
const overrides = parseTemplateOverrides(str.slice(separatorIndex + 1), context);
|
|
495
|
+
return { baseId, overrides };
|
|
496
|
+
}
|
|
497
|
+
// Split a string on a separator while ignoring separators inside [] / {}
|
|
498
|
+
// brackets, so commas inside composite values stay intact.
|
|
499
|
+
function splitBracketAware(str, separator) {
|
|
500
|
+
const parts = [];
|
|
501
|
+
let depth = 0;
|
|
502
|
+
let start = 0;
|
|
503
|
+
for (let i = 0; i < str.length; i++) {
|
|
504
|
+
const ch = str[i];
|
|
505
|
+
if (ch === "[" || ch === "{") {
|
|
506
|
+
depth++;
|
|
507
|
+
}
|
|
508
|
+
else if (ch === "]" || ch === "}") {
|
|
509
|
+
if (depth > 0)
|
|
510
|
+
depth--;
|
|
511
|
+
}
|
|
512
|
+
else if (ch === separator && depth === 0) {
|
|
513
|
+
parts.push(str.slice(start, i));
|
|
514
|
+
start = i + 1;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
parts.push(str.slice(start));
|
|
518
|
+
return parts;
|
|
519
|
+
}
|
|
520
|
+
// Parse the override part of a Template entry. Scalar pairs reuse the Object
|
|
521
|
+
// rule ("key:value"); a stray segment without a key (e.g. the "2.07" produced
|
|
522
|
+
// by writing "Params:6,2.07" without brackets) is reported instead of being
|
|
523
|
+
// silently dropped.
|
|
524
|
+
function parseTemplateOverrides(str, context) {
|
|
525
|
+
const overrides = {};
|
|
526
|
+
for (const pair of splitBracketAware(str, ",")) {
|
|
527
|
+
const colonIndex = pair.indexOf(":");
|
|
528
|
+
const trimmedPair = pair.trim();
|
|
529
|
+
if (colonIndex <= 0) {
|
|
530
|
+
if (trimmedPair !== "" && context) {
|
|
531
|
+
console.warn(`[config2ts] warning: ${context.moduleName} row ${context.row} field "${context.field}" template override segment "${trimmedPair}" is not a key:value pair and was ignored; wrap array values in brackets, e.g. Params:[6,2.07]`);
|
|
532
|
+
}
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const key = pair.slice(0, colonIndex).trim();
|
|
536
|
+
overrides[key] = parseOverrideValue(pair.slice(colonIndex + 1));
|
|
537
|
+
}
|
|
538
|
+
return overrides;
|
|
539
|
+
}
|
|
540
|
+
// Parse one override value: "[...]" resolves to an array (elements inferred
|
|
541
|
+
// like Object values, flat {...} elements are supported), "{...}" resolves to
|
|
542
|
+
// a flat object (same rule as the Object type), and anything else resolves to
|
|
543
|
+
// a scalar (number/boolean/string).
|
|
544
|
+
function parseOverrideValue(value) {
|
|
545
|
+
const trimmed = value.trim();
|
|
546
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
547
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
548
|
+
if (inner === "") {
|
|
549
|
+
return [];
|
|
550
|
+
}
|
|
551
|
+
return splitBracketAware(inner, ",").map((element) => {
|
|
552
|
+
const elementTrimmed = element.trim();
|
|
553
|
+
if (elementTrimmed.startsWith("{") && elementTrimmed.endsWith("}")) {
|
|
554
|
+
return parseObject(elementTrimmed.slice(1, -1));
|
|
555
|
+
}
|
|
556
|
+
return parseObjectValue(elementTrimmed);
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
560
|
+
return parseObject(trimmed.slice(1, -1));
|
|
561
|
+
}
|
|
562
|
+
return parseObjectValue(trimmed);
|
|
563
|
+
}
|
|
564
|
+
// Serialize one parsed Template entry to runtime code. Entries without
|
|
565
|
+
// overrides emit a plain ref (same as Ref: `Module.Map["id"]`); entries with
|
|
566
|
+
// overrides spread the base record and apply the overrides on top. Because
|
|
567
|
+
// the spread result is contextually typed as the target Record, a typo'd
|
|
568
|
+
// override key or an incompatible value type fails at tsc compile time.
|
|
569
|
+
function serializeTemplateEntry(entry, refModuleName) {
|
|
570
|
+
const baseRef = `${refModuleName}.Map[${JSON.stringify(entry.baseId)}]`;
|
|
571
|
+
const overrideKeys = Object.keys(entry.overrides);
|
|
572
|
+
if (overrideKeys.length === 0) {
|
|
573
|
+
return baseRef;
|
|
574
|
+
}
|
|
575
|
+
const overrideStr = overrideKeys
|
|
576
|
+
.map((key) => `${serializeField(key)}:${json5.stringify(entry.overrides[key])}`)
|
|
577
|
+
.join(",");
|
|
578
|
+
return `{...${baseRef},${overrideStr}}`;
|
|
579
|
+
}
|
|
412
580
|
function GetFileExt(filePath) {
|
|
413
581
|
const pathObject = path.parse(filePath);
|
|
414
582
|
return pathObject.ext.slice(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "config2ts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Generate type-safe TypeScript from CSV/INI/TOML config tables — inferred types, enums, cross-table references, and a typed asset index, all from one CLI command.",
|
|
5
5
|
"main": "lib/config2ts.js",
|
|
6
6
|
"types": "lib/config2ts.d.ts",
|