config2ts 2.6.1 → 2.7.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 +4 -2
- package/lib/config2ts.js +158 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,10 +36,12 @@ config2ts -d config -o dist -n csv.ts -a public
|
|
|
36
36
|
* `Enum[]` union contains every value actually present in the data (including `""` for empty slots)
|
|
37
37
|
* `EnumIndex` will generate index type, and will use `Enum` type to generate interface
|
|
38
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
|
|
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
|
|
40
|
+
* Merge order is resolved automatically: tables are topologically sorted so every referenced table (`Ref`/`RefEnum`/`Template`) is emitted before the tables referencing it — no filename tricks needed; tables without a dependency relation keep alphabetical order. Circular references (`a.csv -> b.csv -> a.csv`) and references to missing files print a warning
|
|
40
41
|
* `Object` parse `key:value,key:value` format, auto-infer value types (number/boolean/string), generates a dedicated `type` that merges all keys
|
|
41
42
|
* `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[]`
|
|
42
|
-
* Array convention: primitive arrays (`String[]`/`Number[]`/`Enum[]`/`RefEnum[...] []`) separate elements with `,` and `Object[]` separates objects with `;`; raw cell values are normalized (
|
|
43
|
+
* Array convention: primitive arrays (`String[]`/`Number[]`/`Enum[]`/`RefEnum[...] []`) separate elements with `,` and `Object[]` separates objects with `;`; raw cell values are normalized (CRLF/CR unified to LF, 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
|
|
44
|
+
* Line breaks inside a cell (quote-wrapped in CSV) are preserved for `String` fields (emitted as `\n`) and act as the equivalent separator in structural fields — a line feed equals a comma between array elements / Object pairs / Template override pairs, and equals a semicolon between Object[] elements / Template[] entries (one entry per line works; a blank line is an empty slot by the n+1 rule). Scalar fields (Number/Boolean/Enum/single Ref) have no separator semantics, so a line break there prints a warning and must be removed
|
|
43
45
|
* unrecognized field types fall back to `string` and print a warning (check for typos)
|
|
44
46
|
|
|
45
47
|
## assets2ts
|
package/lib/config2ts.js
CHANGED
|
@@ -57,10 +57,9 @@ const RefEnumPrefix = "RefEnum[";
|
|
|
57
57
|
const RefEnumArraySuffix = "][]";
|
|
58
58
|
const TemplatePrefix = "Template[";
|
|
59
59
|
// Inside a Template cell, '|' separates the referenced base row id from the
|
|
60
|
-
// Object-style override pairs
|
|
61
|
-
//
|
|
60
|
+
// Object-style override pairs. Entries in the array form (and elements of
|
|
61
|
+
// Object[]/primitive arrays) are separated by ';' and line feeds.
|
|
62
62
|
const TemplateOverrideSeparator = "|";
|
|
63
|
-
const TemplateEntrySeparator = ";";
|
|
64
63
|
const AutoGenHeader = "// Auto Generated by config2ts, DO NOT EDIT\n\n";
|
|
65
64
|
exports.Convert = {
|
|
66
65
|
ini: function (str, moduleName) {
|
|
@@ -98,6 +97,15 @@ function csv2ts(csvString, moduleName) {
|
|
|
98
97
|
else {
|
|
99
98
|
for (const k in d) {
|
|
100
99
|
d[k] = normalizeCell(d[k]);
|
|
100
|
+
// Line feeds are preserved on purpose for free-form String fields
|
|
101
|
+
// (multi-line text such as i18n descriptions), and structural types
|
|
102
|
+
// treat them as entry/pair separators (one element or key:value pair
|
|
103
|
+
// per line). Scalar types (Number/Boolean/Enum/single Ref) have no
|
|
104
|
+
// separator semantics, so a line break there is meaningless — warn
|
|
105
|
+
// and let the author remove it from the cell.
|
|
106
|
+
if (convert[k] !== "String" && convert[k] !== "" && !isStructuralType(convert[k]) && d[k].includes("\n")) {
|
|
107
|
+
console.warn(`[config2ts] warning: ${moduleName} row ${i} field "${k}" of type "${convert[k]}" contains a line break inside the cell; line breaks are only supported in String fields and structural (array/Object/Template) fields, please remove it`);
|
|
108
|
+
}
|
|
101
109
|
if (convert[k] === ObjectStr) {
|
|
102
110
|
d[k] = parseObject(d[k]);
|
|
103
111
|
}
|
|
@@ -141,11 +149,17 @@ function csv2ts(csvString, moduleName) {
|
|
|
141
149
|
const template = parseTemplate(convert[k]);
|
|
142
150
|
const templateContext = { moduleName, field: k, row: i };
|
|
143
151
|
if (template.isArray) {
|
|
144
|
-
|
|
152
|
+
// Semicolons and line feeds both separate template entries (one
|
|
153
|
+
// entry per line works in multi-line cells); override pairs inside
|
|
154
|
+
// an entry stay on the same line.
|
|
155
|
+
const entries = d[k] === "" ? [] : d[k].split(/[;\n]/).map((part) => parseTemplateEntry(part, templateContext));
|
|
145
156
|
for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
|
|
146
157
|
if (entries[entryIndex].baseId === "") {
|
|
147
158
|
console.warn(`[config2ts] warning: ${moduleName} row ${i} field "${k}" template array entry ${entryIndex + 1} has an empty base id`);
|
|
148
159
|
}
|
|
160
|
+
else if (entries[entryIndex].baseId.includes(":")) {
|
|
161
|
+
console.warn(`[config2ts] warning: ${moduleName} row ${i} field "${k}" template array entry ${entryIndex + 1} "${entries[entryIndex].baseId}" looks like a key:value pair; each line/semicolon slot must be a full "<baseId>|<overrides>" entry, keep override pairs on the same line`);
|
|
162
|
+
}
|
|
149
163
|
}
|
|
150
164
|
d[k] = entries;
|
|
151
165
|
}
|
|
@@ -335,7 +349,9 @@ function parseObject(str) {
|
|
|
335
349
|
if (trimmed === "") {
|
|
336
350
|
return result;
|
|
337
351
|
}
|
|
338
|
-
|
|
352
|
+
// Commas and line feeds both separate key:value pairs, so one pair per
|
|
353
|
+
// line works in multi-line cells (e.g. "num:1\nstr:a").
|
|
354
|
+
const pairs = trimmed.split(/[,\n]/);
|
|
339
355
|
for (const pair of pairs) {
|
|
340
356
|
const colonIndex = pair.indexOf(":");
|
|
341
357
|
if (colonIndex > 0) {
|
|
@@ -346,10 +362,13 @@ function parseObject(str) {
|
|
|
346
362
|
}
|
|
347
363
|
return result;
|
|
348
364
|
}
|
|
349
|
-
// Normalize a raw cell value:
|
|
350
|
-
//
|
|
365
|
+
// Normalize a raw cell value: convert CRLF/CR line breaks to LF and trim
|
|
366
|
+
// surrounding whitespace. Line feeds inside a quoted cell are intentional
|
|
367
|
+
// (multi-line cells commonly use LF as their entry separator, e.g. one effect
|
|
368
|
+
// per line), so they must be preserved — only the Windows/old-Mac line-break
|
|
369
|
+
// variants are unified to LF.
|
|
351
370
|
function normalizeCell(str) {
|
|
352
|
-
return String(str).replace(/\r\n
|
|
371
|
+
return String(str).replace(/\r\n?/g, "\n").trim();
|
|
353
372
|
}
|
|
354
373
|
// Split a comma-separated primitive-array cell: n commas make n+1 slots and every
|
|
355
374
|
// slot is kept (including an interior or trailing empty one), so parallel arrays
|
|
@@ -362,18 +381,20 @@ function splitStringArray(str) {
|
|
|
362
381
|
if (str === "") {
|
|
363
382
|
return [];
|
|
364
383
|
}
|
|
365
|
-
|
|
384
|
+
// Commas and line feeds both separate elements, so one value per line
|
|
385
|
+
// works in multi-line cells (e.g. "a\nb" -> ['a','b']).
|
|
386
|
+
return str.split(/[,\n]/).map((v) => v.trim());
|
|
366
387
|
}
|
|
367
|
-
// Object[] separates objects with semicolons (commas are reserved
|
|
368
|
-
// key:value pairs inside an object, same syntax as the Object type); each
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
388
|
+
// Object[] separates objects with semicolons or line feeds (commas are reserved
|
|
389
|
+
// for the key:value pairs inside an object, same syntax as the Object type); each
|
|
390
|
+
// slot is parsed by the same object rule, so stray empty pairs within an object
|
|
391
|
+
// (e.g. "num:1,,str:a") are ignored while an empty object slot between separators
|
|
392
|
+
// (e.g. "a:1;;b:2" or a blank line) becomes an empty object placeholder.
|
|
372
393
|
function parseObjectArray(str) {
|
|
373
394
|
if (str === "") {
|
|
374
395
|
return [];
|
|
375
396
|
}
|
|
376
|
-
return str.split(
|
|
397
|
+
return str.split(/[;\n]/).map((part) => parseObject(part));
|
|
377
398
|
}
|
|
378
399
|
function parseObjectValue(value) {
|
|
379
400
|
if (value === "true")
|
|
@@ -417,6 +438,27 @@ function isKnownType(typeStr) {
|
|
|
417
438
|
}
|
|
418
439
|
return false;
|
|
419
440
|
}
|
|
441
|
+
// Structural types split cell content on separators (commas/semicolons/pipes),
|
|
442
|
+
// so a line feed can act as the equivalent separator and one entry per line is
|
|
443
|
+
// valid in multi-line cells. Scalar types (Number/Boolean/Enum/single Ref) have
|
|
444
|
+
// no separator semantics, so a line feed in those cells is an error.
|
|
445
|
+
function isStructuralType(typeStr) {
|
|
446
|
+
if (typeStr === ObjectStr || typeStr === ObjectArrayString || typeStr === "String[]" || typeStr === "Number[]" || typeStr === EnumArrayString) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
const ref = parseRef(typeStr);
|
|
450
|
+
if (ref && ref.isArray) {
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
const refEnum = parseRefEnum(typeStr);
|
|
454
|
+
if (refEnum && refEnum.isArray) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
if (parseTemplate(typeStr)) {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
420
462
|
// Parse Ref[module] (single) and Ref[module][] (array of row references).
|
|
421
463
|
// Note: "RefEnum[...]" starts with "RefE", so it never collides with "Ref[".
|
|
422
464
|
function parseRef(typeStr) {
|
|
@@ -494,9 +536,11 @@ function parseTemplateEntry(str, context) {
|
|
|
494
536
|
const overrides = parseTemplateOverrides(str.slice(separatorIndex + 1), context);
|
|
495
537
|
return { baseId, overrides };
|
|
496
538
|
}
|
|
497
|
-
// Split a string on
|
|
498
|
-
// brackets, so commas inside composite values stay intact.
|
|
499
|
-
|
|
539
|
+
// Split a string on any character in `separators` while ignoring separators
|
|
540
|
+
// inside [] / {} brackets, so commas inside composite values stay intact.
|
|
541
|
+
// Line feeds are accepted as separators too, so one override pair / array
|
|
542
|
+
// element per line works in multi-line cells.
|
|
543
|
+
function splitBracketAware(str, separators) {
|
|
500
544
|
const parts = [];
|
|
501
545
|
let depth = 0;
|
|
502
546
|
let start = 0;
|
|
@@ -509,7 +553,7 @@ function splitBracketAware(str, separator) {
|
|
|
509
553
|
if (depth > 0)
|
|
510
554
|
depth--;
|
|
511
555
|
}
|
|
512
|
-
else if (ch
|
|
556
|
+
else if (separators.includes(ch) && depth === 0) {
|
|
513
557
|
parts.push(str.slice(start, i));
|
|
514
558
|
start = i + 1;
|
|
515
559
|
}
|
|
@@ -523,7 +567,7 @@ function splitBracketAware(str, separator) {
|
|
|
523
567
|
// silently dropped.
|
|
524
568
|
function parseTemplateOverrides(str, context) {
|
|
525
569
|
const overrides = {};
|
|
526
|
-
for (const pair of splitBracketAware(str, "
|
|
570
|
+
for (const pair of splitBracketAware(str, ",\n")) {
|
|
527
571
|
const colonIndex = pair.indexOf(":");
|
|
528
572
|
const trimmedPair = pair.trim();
|
|
529
573
|
if (colonIndex <= 0) {
|
|
@@ -548,7 +592,7 @@ function parseOverrideValue(value) {
|
|
|
548
592
|
if (inner === "") {
|
|
549
593
|
return [];
|
|
550
594
|
}
|
|
551
|
-
return splitBracketAware(inner, "
|
|
595
|
+
return splitBracketAware(inner, ",\n").map((element) => {
|
|
552
596
|
const elementTrimmed = element.trim();
|
|
553
597
|
if (elementTrimmed.startsWith("{") && elementTrimmed.endsWith("}")) {
|
|
554
598
|
return parseObject(elementTrimmed.slice(1, -1));
|
|
@@ -712,8 +756,100 @@ function GetTsString(filePath) {
|
|
|
712
756
|
return "";
|
|
713
757
|
}
|
|
714
758
|
}
|
|
759
|
+
// Collect the config file names a CSV references from its type row
|
|
760
|
+
// (Ref[file], RefEnum[file.field], Template[file] and their array forms).
|
|
761
|
+
// ini/toml files never reference other config tables.
|
|
762
|
+
function collectCsvReferences(filePath) {
|
|
763
|
+
if (GetFileExt(filePath) !== "csv") {
|
|
764
|
+
return [];
|
|
765
|
+
}
|
|
766
|
+
let fileString = fs.readFileSync(filePath).toString();
|
|
767
|
+
if (fileString.charAt(0) === "\uFEFF") {
|
|
768
|
+
fileString = fileString.substr(1);
|
|
769
|
+
}
|
|
770
|
+
const references = [];
|
|
771
|
+
(0, d3_dsv_1.csvParse)(fileString, function (d, i) {
|
|
772
|
+
if (i === 0) {
|
|
773
|
+
for (const k in d) {
|
|
774
|
+
const typeStr = d[k];
|
|
775
|
+
const ref = parseRef(typeStr);
|
|
776
|
+
const refEnum = parseRefEnum(typeStr);
|
|
777
|
+
const template = parseTemplate(typeStr);
|
|
778
|
+
const target = ref ? ref.refModule : refEnum ? refEnum.refModule : template ? template.refModule : null;
|
|
779
|
+
if (target && !references.includes(target)) {
|
|
780
|
+
references.push(target);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return null;
|
|
785
|
+
});
|
|
786
|
+
return references;
|
|
787
|
+
}
|
|
788
|
+
// Order files so every referenced table is emitted before the table that
|
|
789
|
+
// references it (a referenced namespace must exist when the referencing
|
|
790
|
+
// table's data initializes), so authors no longer need filename tricks (e.g.
|
|
791
|
+
// an "!" prefix) to control order. Files without a dependency relation keep
|
|
792
|
+
// their alphabetical order. References to missing files and circular
|
|
793
|
+
// references are reported; a cycle is broken at the back edge so output is
|
|
794
|
+
// still generated (the runtime data still needs the cycle fixed by hand).
|
|
795
|
+
function sortFilesByReferences(fileList) {
|
|
796
|
+
const ordered = fileList.slice().sort((a, b) => {
|
|
797
|
+
const baseA = path.basename(a);
|
|
798
|
+
const baseB = path.basename(b);
|
|
799
|
+
return baseA < baseB ? -1 : baseA > baseB ? 1 : 0;
|
|
800
|
+
});
|
|
801
|
+
const fileByBase = new Map();
|
|
802
|
+
for (const filePath of ordered) {
|
|
803
|
+
fileByBase.set(path.basename(filePath), filePath);
|
|
804
|
+
}
|
|
805
|
+
const depsByBase = new Map();
|
|
806
|
+
for (const filePath of ordered) {
|
|
807
|
+
const base = path.basename(filePath);
|
|
808
|
+
const deps = collectCsvReferences(filePath);
|
|
809
|
+
depsByBase.set(base, deps);
|
|
810
|
+
for (const dep of deps) {
|
|
811
|
+
if (!fileByBase.has(dep)) {
|
|
812
|
+
console.warn(`[config2ts] warning: ${base} references "${dep}" but no config file with that name was found in the config directory`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const sorted = [];
|
|
817
|
+
// 0 = unvisited, 1 = visiting (on the current DFS trail), 2 = done
|
|
818
|
+
const state = new Map();
|
|
819
|
+
const reportedCycles = new Set();
|
|
820
|
+
function visit(base, trail) {
|
|
821
|
+
var _a, _b;
|
|
822
|
+
const current = (_a = state.get(base)) !== null && _a !== void 0 ? _a : 0;
|
|
823
|
+
if (current === 2) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (current === 1) {
|
|
827
|
+
const cycleStart = trail.indexOf(base);
|
|
828
|
+
const cycleKey = trail.slice(cycleStart).concat(base).join(" -> ");
|
|
829
|
+
if (!reportedCycles.has(cycleKey)) {
|
|
830
|
+
reportedCycles.add(cycleKey);
|
|
831
|
+
console.warn(`[config2ts] warning: circular table reference detected: ${cycleKey}; tables in the cycle may reference an undefined namespace at runtime, please break the reference cycle`);
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
state.set(base, 1);
|
|
836
|
+
trail.push(base);
|
|
837
|
+
for (const dep of (_b = depsByBase.get(base)) !== null && _b !== void 0 ? _b : []) {
|
|
838
|
+
if (fileByBase.has(dep)) {
|
|
839
|
+
visit(dep, trail);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
trail.pop();
|
|
843
|
+
state.set(base, 2);
|
|
844
|
+
sorted.push(fileByBase.get(base));
|
|
845
|
+
}
|
|
846
|
+
for (const filePath of ordered) {
|
|
847
|
+
visit(path.basename(filePath), []);
|
|
848
|
+
}
|
|
849
|
+
return sorted;
|
|
850
|
+
}
|
|
715
851
|
function GetTsStringFromFileList(fileList) {
|
|
716
|
-
return fileList
|
|
852
|
+
return sortFilesByReferences(fileList)
|
|
717
853
|
.map(function (filePath) {
|
|
718
854
|
return GetTsString(filePath);
|
|
719
855
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "config2ts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
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",
|