config2ts 2.8.1 → 2.10.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 +227 -117
- package/lib/config2ts.d.ts +17 -2
- package/lib/config2ts.js +520 -82
- package/lib/main.js +8 -3
- package/package.json +7 -16
package/README.md
CHANGED
|
@@ -1,117 +1,227 @@
|
|
|
1
|
-
# config2ts
|
|
2
|
-
|
|
3
|
-
convert config to ts file.
|
|
4
|
-
|
|
5
|
-
# install
|
|
6
|
-
|
|
7
|
-
run command: `npm install -g config2ts`
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
import
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
1
|
+
# config2ts
|
|
2
|
+
|
|
3
|
+
convert config to ts file.
|
|
4
|
+
|
|
5
|
+
# install
|
|
6
|
+
|
|
7
|
+
run command: `npm install -g config2ts`
|
|
8
|
+
|
|
9
|
+
# documentation
|
|
10
|
+
|
|
11
|
+
Full documentation is published via GitHub Pages (this repo has Pages enabled):
|
|
12
|
+
|
|
13
|
+
- English: https://livingyang.github.io/config2ts/#/docs/en
|
|
14
|
+
- 中文: https://livingyang.github.io/config2ts/#/docs/zh
|
|
15
|
+
|
|
16
|
+
Both the documentation pages and the per-feature demo cases are served by the
|
|
17
|
+
Vue site in [`website/`](website/) (built into `docs/`).
|
|
18
|
+
|
|
19
|
+
# usage
|
|
20
|
+
|
|
21
|
+
`config2ts` supports two output modes selected by `-m, --mode`:
|
|
22
|
+
|
|
23
|
+
- `single` (default): merge every table into one file (`-n`, default `csv.ts`).
|
|
24
|
+
- `split`: write one `.ts` file per table plus an auto-generated `index.ts`.
|
|
25
|
+
|
|
26
|
+
Every case under [`fixtures/`](fixtures/) pairs its source tables with the
|
|
27
|
+
single-mode merge of those tables, so you can compare input and output directly
|
|
28
|
+
(the demo site renders the same pairs):
|
|
29
|
+
|
|
30
|
+
- single mode example: [`fixtures/03-cross-ref/expected.ts`](fixtures/03-cross-ref/expected.ts)
|
|
31
|
+
- split mode: one file per table plus `index.ts` (layout shown below)
|
|
32
|
+
|
|
33
|
+
## fixtures (single source of truth)
|
|
34
|
+
|
|
35
|
+
The [`fixtures/`](fixtures/) folder holds one self-contained case directory per
|
|
36
|
+
feature (`01-basic-types`, `03-cross-ref`, ...). Each case contains its source
|
|
37
|
+
tables plus a committed golden file `expected.ts` generated from them. The same
|
|
38
|
+
`(input.csv, expected.ts)` pairs are used by the golden-file unit tests
|
|
39
|
+
([`tests/fixtures.test.ts`](tests/fixtures.test.ts)) and by the demo site, so
|
|
40
|
+
documentation, tests and demos can never drift from actual tool behavior.
|
|
41
|
+
|
|
42
|
+
A case may also use subdirectories: when every csv file inside a subdirectory
|
|
43
|
+
shares the same header and type rows, they are merged into ONE table named
|
|
44
|
+
after the directory (`fixtures/08-story/story/*.csv` → `StoryCsv`) — see
|
|
45
|
+
[fixtures/08-story](fixtures/08-story) and the "Isomorphic subdirectory merge"
|
|
46
|
+
section of the docs.
|
|
47
|
+
|
|
48
|
+
- Regenerate golden files: `npm run fixtures:regen` (rebuilds lib, then rewrites
|
|
49
|
+
every `expected.ts`) — review the diff manually before committing; never
|
|
50
|
+
blindly rewrite a failing golden file.
|
|
51
|
+
- Tests: every case is regenerated in-memory and compared byte-for-byte against
|
|
52
|
+
its golden file, and all golden files are additionally checked with `tsc`.
|
|
53
|
+
- [`tests/golden/merged-formats.expected.ts`](tests/golden/merged-formats.expected.ts)
|
|
54
|
+
is a composite golden: it merges tables from several cases (CSV with refs +
|
|
55
|
+
INI + TOML) in a single call, which per-case goldens can't cover. It is
|
|
56
|
+
regenerated by the same `fixtures:regen` run.
|
|
57
|
+
|
|
58
|
+
## demo site
|
|
59
|
+
|
|
60
|
+
[`website/`](website/) is a Vue3 + Vite site that renders every fixture case as
|
|
61
|
+
an input → output comparison page. It reads the same `fixtures/` pairs the tests
|
|
62
|
+
verify via `import.meta.glob(..., { query: "?raw" })` — no copies, no codegen —
|
|
63
|
+
so the demo can never drift from actual tool behavior.
|
|
64
|
+
|
|
65
|
+
- Local dev: `npm run demo:dev`
|
|
66
|
+
- Build: `npm run demo:build` — output goes to `docs/`, published on GitHub
|
|
67
|
+
Pages at `/config2ts/` (the site owns the Pages root; `docs/` holds only the
|
|
68
|
+
build output)
|
|
69
|
+
|
|
70
|
+
## single mode
|
|
71
|
+
|
|
72
|
+
Merge all config tables into a single TypeScript file.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
config2ts -d fixtures/03-cross-ref -o dist -m single -n csv.ts
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- `-n` is the merged output file name (default `csv.ts`).
|
|
79
|
+
- `-a` is optional; omit it to skip `assets.ts` (e.g. `-a public` indexes this
|
|
80
|
+
repo's sample asset tree).
|
|
81
|
+
- Example output: [`fixtures/03-cross-ref/expected.ts`](fixtures/03-cross-ref/expected.ts) — all tables as `export namespace XxxCsv { ... }` in one file.
|
|
82
|
+
|
|
83
|
+
## split mode
|
|
84
|
+
|
|
85
|
+
Write one `.ts` file per config table (named after the source file, e.g.
|
|
86
|
+
`skill.csv` → `skill.ts`) plus an auto-generated `index.ts` that re-exports every
|
|
87
|
+
table and `assets.ts`. Cross-table `Ref`/`RefEnum`/`Template` references are
|
|
88
|
+
resolved with `import` statements injected at the top of the referencing file, so
|
|
89
|
+
every table is independently usable and tree-shakable.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
config2ts -d fixtures/03-cross-ref -o dist -m split -a public
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- `-n` is reused as the index file name; the single-mode default `csv.ts` is
|
|
96
|
+
replaced by `index.ts` unless you pass `-n` explicitly.
|
|
97
|
+
- `-a` is optional; omit it to skip `assets.ts`.
|
|
98
|
+
- Example output: one file per table (`z-base.ts`, `a-user.ts`, `skill.ts`, …),
|
|
99
|
+
plus `assets.ts` and `index.ts` (layout below).
|
|
100
|
+
|
|
101
|
+
Output layout under `dist`:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
dist/
|
|
105
|
+
z-base.ts # export namespace ZBaseCsv { ... }
|
|
106
|
+
a-user.ts # import { ZBaseCsv } from "./z-base"; export namespace AUserCsv { ... }
|
|
107
|
+
skill.ts
|
|
108
|
+
...
|
|
109
|
+
assets.ts # generated only when -a is given
|
|
110
|
+
index.ts # export * from "./z-base"; export * from "./a-user"; ...; export * from "./assets";
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Notes:
|
|
114
|
+
- Tables are emitted in topological order; a table only `import`s tables it
|
|
115
|
+
actually references, and missing/circular references print the same warnings as
|
|
116
|
+
single mode.
|
|
117
|
+
|
|
118
|
+
# support type
|
|
119
|
+
|
|
120
|
+
| csv field | typescript type |
|
|
121
|
+
| :--------: | :---------------: |
|
|
122
|
+
| Index | string |
|
|
123
|
+
| String | string |
|
|
124
|
+
| Number | number |
|
|
125
|
+
| Boolean | boolean |
|
|
126
|
+
| Enum | type |
|
|
127
|
+
| EnumIndex | type |
|
|
128
|
+
| String[] | string[] |
|
|
129
|
+
| Number[] | number[] |
|
|
130
|
+
| Enum[] | type[] |
|
|
131
|
+
| Ref | namespace.Record |
|
|
132
|
+
| RefEnum | namespace.type |
|
|
133
|
+
| RefEnum[] | namespace.type[] |
|
|
134
|
+
| RefTable | namespace.Record[] |
|
|
135
|
+
| RefTable[] | namespace.Record[][] |
|
|
136
|
+
| Template | namespace.Record |
|
|
137
|
+
| Template[] | namespace.Record[] |
|
|
138
|
+
| Object | type (Record) |
|
|
139
|
+
| Object[] | type[] (Record[]) |
|
|
140
|
+
|
|
141
|
+
* `Number` support Infinity and NaN
|
|
142
|
+
* `Enum` support empty string type
|
|
143
|
+
* `Enum[]` union contains every value actually present in the data (including `""` for empty slots)
|
|
144
|
+
* `EnumIndex` will generate index type, and will use `Enum` type to generate interface
|
|
145
|
+
* `Ref` / `RefEnum` reference another csv's Record/enum by id: `Ref[table]` is one row (`table.Map["id"]`), `Ref[table][]` is an array of rows with comma-separated ids (`[table.Map["1"],table.Map["2"]]`, typed `File.Record[]`); use `Object[]` for inline private structures and `Ref[]` for shared, identity-bearing rows
|
|
146
|
+
* `Template[table]` 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 `{ ...table.Map["101"], damage: 150, range: 8 }` typed `File.Record`; with no overrides the cell is just the id and emits a plain `table.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[table][]` separates entries with `;` (the same element separator as `Object[]`), e.g. `101|damage:150;102|heal:500` emits `[{ ...table.Map["101"], damage: 150 },{ ...table.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
|
|
147
|
+
* Reference targets are written as **table names without the extension** (`Ref[skill]` → `skill.csv`); the full file name (`Ref[skill.csv]`) still works, and an ambiguous stem (`skill.csv` + `skill.ini`) warns and needs the full name
|
|
148
|
+
* **Homogeneous subdirectories merge into one table named after the directory**: when every csv in a subdirectory shares the same header and type rows, `story/ch01.csv` … → `StoryCsv`, rows concatenated in file-name order. The merged table has no flat `List`; it exports `FileMap` grouped by member file plus `type File = "ch01" | ...` and `refTable(file: File)`; `RefTable[story]` references a whole member sub-table by file name (typed `StoryCsv.Record[]`, `File` union makes an unknown name a compile error). Member files that nothing references are allowed. See [`fixtures/08-story`](fixtures/08-story)
|
|
149
|
+
* Merge order is resolved automatically: tables are topologically sorted so every referenced table (`Ref`/`RefEnum`/`Template`/`RefTable`) 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
|
|
150
|
+
* `Object` parse `key:value,key:value` format, auto-infer value types (number/boolean/string), generates a dedicated `type` that merges all keys
|
|
151
|
+
* `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[]`
|
|
152
|
+
* 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
|
|
153
|
+
* 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
|
|
154
|
+
* unrecognized field types fall back to `string` and print a warning (check for typos)
|
|
155
|
+
|
|
156
|
+
## assets2ts
|
|
157
|
+
|
|
158
|
+
Scan assets directory and generate an `assets.ts` index file with nested resource tree.
|
|
159
|
+
|
|
160
|
+
- `-a, --assets <path>` set assets resource directory (optional, skip asset index if omitted)
|
|
161
|
+
- Output file: `assets.ts` (alongside the merged config file)
|
|
162
|
+
- File and directory names are used exactly as-is (e.g., `Direction.png` → `Direction`, `adjust-horizontal.png` → `'adjust-horizontal'`); keys with special characters are automatically quoted
|
|
163
|
+
- `type` field uses file extension (lowercase), e.g. `'png'`, `'mp3'`, `'svg'`
|
|
164
|
+
- Directories containing 2+ files of the same extension get a dedicated type (e.g. `PngAsset`, `Mp3Asset`) and `satisfies Record<string, XxxAsset>` annotation
|
|
165
|
+
- Directories with ≤1 file or mixed extensions do not get a type annotation
|
|
166
|
+
- Nested directories are supported (e.g. `public/sub/image/`)
|
|
167
|
+
|
|
168
|
+
### generated example
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// Auto Generated by config2ts, DO NOT EDIT
|
|
172
|
+
|
|
173
|
+
export type Mp3Asset = { path: string; type: 'mp3' };
|
|
174
|
+
export type PngAsset = { path: string; type: 'png' };
|
|
175
|
+
export type SvgAsset = { path: string; type: 'svg' };
|
|
176
|
+
|
|
177
|
+
export const ASSETS = {
|
|
178
|
+
public: {
|
|
179
|
+
image: {
|
|
180
|
+
'adjust-horizontal': {path:'public/image/adjust-horizontal.png',type:'png'},
|
|
181
|
+
Direction: {path:'public/image/Direction.png',type:'png'}
|
|
182
|
+
} satisfies Record<string, PngAsset>,
|
|
183
|
+
music: {
|
|
184
|
+
effect1: {path:'public/music/effect1.mp3',type:'mp3'},
|
|
185
|
+
effect2: {path:'public/music/effect2.mp3',type:'mp3'}
|
|
186
|
+
} satisfies Record<string, Mp3Asset>,
|
|
187
|
+
'single-file': {
|
|
188
|
+
effect1: {path:'public/single-file/effect1.mp3',type:'mp3'}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### usage
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { ASSETS } from "./assets";
|
|
198
|
+
|
|
199
|
+
const meta = ASSETS.public.image.Direction;
|
|
200
|
+
// meta.path → 'public/image/Direction.png'
|
|
201
|
+
// meta.type → 'png'
|
|
202
|
+
|
|
203
|
+
const adjustMeta = ASSETS.public.image['adjust-horizontal'];
|
|
204
|
+
// adjustMeta.path → 'public/image/adjust-horizontal.png'
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Options
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
Options:
|
|
211
|
+
|
|
212
|
+
-h, --help output usage information
|
|
213
|
+
-V, --version output the version number
|
|
214
|
+
-d, --dir <path> set convert path. default: ./
|
|
215
|
+
-o, --outDir <path> set outDir path. default: same as -d
|
|
216
|
+
-n, --name <name> output file name (single mode) / index file name (split mode). default: csv.ts
|
|
217
|
+
-a, --assets <path> set assets resource directory for asset index (optional, skip asset index if omitted)
|
|
218
|
+
-m, --mode <mode> output mode: single (merge into one file) or split (one file per table + index.ts). default: single
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## supported file formats
|
|
222
|
+
|
|
223
|
+
| format | extension |
|
|
224
|
+
| :----: | :-------: |
|
|
225
|
+
| csv | .csv |
|
|
226
|
+
| ini | .ini |
|
|
227
|
+
| toml | .toml |
|
package/lib/config2ts.d.ts
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
type ConvertHandler = (str: string, moduleName: string) => string;
|
|
1
|
+
type ConvertHandler = (str: string, moduleName: string, subTables?: SubTableInfo[]) => string;
|
|
2
2
|
export declare const Convert: Record<string, ConvertHandler>;
|
|
3
3
|
export declare function assets2ts(assetsDir: string): string;
|
|
4
4
|
export declare function GetTsString(filePath: string): string;
|
|
5
|
+
export interface SubTableInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
rowCount: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ConvertUnit {
|
|
10
|
+
name: string;
|
|
11
|
+
ext: string;
|
|
12
|
+
content: string;
|
|
13
|
+
sourcePaths: string[];
|
|
14
|
+
refs: string[];
|
|
15
|
+
subTables?: SubTableInfo[];
|
|
16
|
+
}
|
|
17
|
+
export declare function resolveTableName(ref: string, names: Iterable<string>): string | null;
|
|
18
|
+
export declare function collectConvertUnits(dir: string): ConvertUnit[];
|
|
19
|
+
export declare function GetTsStringFromUnits(units: ConvertUnit[]): string;
|
|
5
20
|
export declare function GetTsStringFromFileList(fileList: string[]): string;
|
|
6
21
|
export declare function GetValidFileList(fileList: string[]): string[];
|
|
7
|
-
export declare function startConvert(dir: string, outDir: string, merge: string, assetsDir?: string): void;
|
|
22
|
+
export declare function startConvert(dir: string, outDir: string, merge: string, assetsDir?: string, mode?: "single" | "split"): void;
|
|
8
23
|
export {};
|
package/lib/config2ts.js
CHANGED
|
@@ -36,6 +36,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.Convert = void 0;
|
|
37
37
|
exports.assets2ts = assets2ts;
|
|
38
38
|
exports.GetTsString = GetTsString;
|
|
39
|
+
exports.resolveTableName = resolveTableName;
|
|
40
|
+
exports.collectConvertUnits = collectConvertUnits;
|
|
41
|
+
exports.GetTsStringFromUnits = GetTsStringFromUnits;
|
|
39
42
|
exports.GetTsStringFromFileList = GetTsStringFromFileList;
|
|
40
43
|
exports.GetValidFileList = GetValidFileList;
|
|
41
44
|
exports.startConvert = startConvert;
|
|
@@ -54,6 +57,7 @@ const ObjectArrayString = "Object[]";
|
|
|
54
57
|
const RefPrefix = "Ref[";
|
|
55
58
|
const RefSuffix = "]";
|
|
56
59
|
const RefEnumPrefix = "RefEnum[";
|
|
60
|
+
const RefTablePrefix = "RefTable[";
|
|
57
61
|
const RefEnumArraySuffix = "][]";
|
|
58
62
|
const TemplatePrefix = "Template[";
|
|
59
63
|
// Inside a Template cell, '|' separates the referenced base row id from the
|
|
@@ -66,18 +70,30 @@ exports.Convert = {
|
|
|
66
70
|
const obj = toml.parse(str);
|
|
67
71
|
return `export const ${moduleName} = ${json5.stringify(obj, null, 4)};`;
|
|
68
72
|
},
|
|
69
|
-
csv: function (str, moduleName) {
|
|
70
|
-
return csv2ts(str, moduleName);
|
|
73
|
+
csv: function (str, moduleName, subTables) {
|
|
74
|
+
return csv2ts(str, moduleName, subTables);
|
|
71
75
|
},
|
|
72
76
|
toml: function (str, moduleName) {
|
|
73
77
|
const obj = toml.parse(str);
|
|
74
78
|
return `export const ${moduleName} = ${json5.stringify(obj, null, 4)};`;
|
|
75
79
|
},
|
|
76
80
|
};
|
|
77
|
-
function csv2ts(csvString, moduleName) {
|
|
81
|
+
function csv2ts(csvString, moduleName, subTables) {
|
|
78
82
|
let convert = {};
|
|
79
83
|
const refFields = [];
|
|
80
84
|
const refEnumFields = [];
|
|
85
|
+
// Sub-table binding for merged subdirectories: each data row is tagged with
|
|
86
|
+
// the member file it came from, so the namespace can export a FileMap
|
|
87
|
+
// (file name -> that file's rows) in addition to the flat List.
|
|
88
|
+
const groupOffsets = [];
|
|
89
|
+
if (subTables && subTables.length > 0) {
|
|
90
|
+
let cursor = 0;
|
|
91
|
+
for (const subTable of subTables) {
|
|
92
|
+
groupOffsets.push({ name: subTable.name, start: cursor, end: cursor + subTable.rowCount });
|
|
93
|
+
cursor += subTable.rowCount;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const rowGroups = [];
|
|
81
97
|
const result = (0, d3_dsv_1.csvParse)(csvString, function (d, i) {
|
|
82
98
|
if (i === 0) {
|
|
83
99
|
convert = d;
|
|
@@ -95,6 +111,11 @@ function csv2ts(csvString, moduleName) {
|
|
|
95
111
|
return null;
|
|
96
112
|
}
|
|
97
113
|
else {
|
|
114
|
+
const dataRowIndex = i - 1;
|
|
115
|
+
if (groupOffsets.length > 0) {
|
|
116
|
+
const group = groupOffsets.find((g) => dataRowIndex >= g.start && dataRowIndex < g.end);
|
|
117
|
+
rowGroups[dataRowIndex] = group ? group.name : undefined;
|
|
118
|
+
}
|
|
98
119
|
for (const k in d) {
|
|
99
120
|
d[k] = normalizeCell(d[k]);
|
|
100
121
|
// Line feeds are preserved on purpose for free-form String fields
|
|
@@ -174,6 +195,19 @@ function csv2ts(csvString, moduleName) {
|
|
|
174
195
|
d[k] = entry;
|
|
175
196
|
}
|
|
176
197
|
}
|
|
198
|
+
else if (parseRefTable(convert[k])) {
|
|
199
|
+
const refTable = parseRefTable(convert[k]);
|
|
200
|
+
if (refTable.isArray) {
|
|
201
|
+
const names = splitStringArray(d[k]);
|
|
202
|
+
d[k] = names;
|
|
203
|
+
if (names.includes("")) {
|
|
204
|
+
console.warn(`[config2ts] warning: ${moduleName} row ${i} field "${k}" ref-table array contains an empty file name`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (d[k] === "") {
|
|
208
|
+
console.warn(`[config2ts] warning: ${moduleName} row ${i} field "${k}" ref-table value is empty`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
177
211
|
else {
|
|
178
212
|
d[k] = String(d[k]);
|
|
179
213
|
}
|
|
@@ -182,6 +216,11 @@ function csv2ts(csvString, moduleName) {
|
|
|
182
216
|
}
|
|
183
217
|
});
|
|
184
218
|
delete result.columns;
|
|
219
|
+
if (groupOffsets.length > 0 && rowGroups.length !== groupOffsets[groupOffsets.length - 1].end) {
|
|
220
|
+
console.warn(`[config2ts] warning: ${moduleName} sub-table row counts do not match the merged content, FileMap will not be generated`);
|
|
221
|
+
groupOffsets.length = 0;
|
|
222
|
+
rowGroups.length = 0;
|
|
223
|
+
}
|
|
185
224
|
if (result.length <= 0) {
|
|
186
225
|
return "";
|
|
187
226
|
}
|
|
@@ -280,6 +319,11 @@ function csv2ts(csvString, moduleName) {
|
|
|
280
319
|
const refModuleName = changeCase.pascalCase(refEnum.refModule);
|
|
281
320
|
fieldType = refEnum.isArray ? `${refModuleName}.${refEnum.refField}[]` : `${refModuleName}.${refEnum.refField}`;
|
|
282
321
|
}
|
|
322
|
+
else if (parseRefTable(convert[field])) {
|
|
323
|
+
const refTable = parseRefTable(convert[field]);
|
|
324
|
+
const refModuleName = changeCase.pascalCase(refTable.refModule);
|
|
325
|
+
fieldType = refTable.isArray ? `${refModuleName}.Record[][]` : `${refModuleName}.Record[]`;
|
|
326
|
+
}
|
|
283
327
|
else if (parseTemplate(convert[field])) {
|
|
284
328
|
const template = parseTemplate(convert[field]);
|
|
285
329
|
const refModuleName = changeCase.pascalCase(template.refModule);
|
|
@@ -294,18 +338,27 @@ function csv2ts(csvString, moduleName) {
|
|
|
294
338
|
}
|
|
295
339
|
template += " };\n\n";
|
|
296
340
|
if (indexField) {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
341
|
+
const kept = [];
|
|
342
|
+
for (let i = 0; i < result.length; i++) {
|
|
343
|
+
if (result[i][indexField] !== "") {
|
|
344
|
+
kept.push(i);
|
|
345
|
+
}
|
|
302
346
|
}
|
|
303
|
-
result
|
|
347
|
+
// Compact result and rowGroups in lockstep so each row keeps its sub-table
|
|
348
|
+
// tag even when empty-index rows are dropped.
|
|
349
|
+
for (let i = 0; i < kept.length; i++) {
|
|
350
|
+
result[i] = result[kept[i]];
|
|
351
|
+
rowGroups[i] = rowGroups[kept[i]];
|
|
352
|
+
}
|
|
353
|
+
result.length = kept.length;
|
|
354
|
+
rowGroups.length = kept.length;
|
|
304
355
|
}
|
|
305
|
-
|
|
356
|
+
// Serialize every surviving row once; the same literals feed both the flat
|
|
357
|
+
// List and the per-file FileMap groups.
|
|
358
|
+
const rowStrings = [];
|
|
306
359
|
for (let i = 0; i < result.length; i++) {
|
|
307
360
|
const row = result[i];
|
|
308
|
-
|
|
361
|
+
let rowString = " {\n";
|
|
309
362
|
for (const field in convert) {
|
|
310
363
|
let value = row[field];
|
|
311
364
|
if (parseRef(convert[field])) {
|
|
@@ -313,10 +366,21 @@ function csv2ts(csvString, moduleName) {
|
|
|
313
366
|
const refModuleName = changeCase.pascalCase(ref.refModule);
|
|
314
367
|
if (ref.isArray) {
|
|
315
368
|
const refs = value.map((id) => `${refModuleName}.Map[${JSON.stringify(id)}]`).join(",");
|
|
316
|
-
|
|
369
|
+
rowString += ` ${serializeField(field)}: [${refs}],\n`;
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
rowString += ` ${serializeField(field)}: ${refModuleName}.Map[${JSON.stringify(value)}],\n`;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else if (parseRefTable(convert[field])) {
|
|
376
|
+
const refTable = parseRefTable(convert[field]);
|
|
377
|
+
const refModuleName = changeCase.pascalCase(refTable.refModule);
|
|
378
|
+
if (refTable.isArray) {
|
|
379
|
+
const names = value.map((name) => `${refModuleName}.refTable(${JSON.stringify(name)})`).join(",");
|
|
380
|
+
rowString += ` ${serializeField(field)}: [${names}],\n`;
|
|
317
381
|
}
|
|
318
382
|
else {
|
|
319
|
-
|
|
383
|
+
rowString += ` ${serializeField(field)}: ${refModuleName}.refTable(${JSON.stringify(value)}),\n`;
|
|
320
384
|
}
|
|
321
385
|
}
|
|
322
386
|
else if (parseTemplate(convert[field])) {
|
|
@@ -324,26 +388,65 @@ function csv2ts(csvString, moduleName) {
|
|
|
324
388
|
const refModuleName = changeCase.pascalCase(templateType.refModule);
|
|
325
389
|
if (templateType.isArray) {
|
|
326
390
|
const entries = value.map((entry) => serializeTemplateEntry(entry, refModuleName)).join(",");
|
|
327
|
-
|
|
391
|
+
rowString += ` ${serializeField(field)}: [${entries}],\n`;
|
|
328
392
|
}
|
|
329
393
|
else {
|
|
330
|
-
|
|
394
|
+
rowString += ` ${serializeField(field)}: ${serializeTemplateEntry(value, refModuleName)},\n`;
|
|
331
395
|
}
|
|
332
396
|
}
|
|
333
397
|
else {
|
|
334
|
-
|
|
398
|
+
rowString += ` ${serializeField(field)}: ${json5.stringify(value)},\n`;
|
|
335
399
|
}
|
|
336
400
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
401
|
+
rowString += " }";
|
|
402
|
+
rowStrings.push(rowString);
|
|
403
|
+
}
|
|
404
|
+
// A merged subdirectory exposes its rows through FileMap only — a flat List
|
|
405
|
+
// would duplicate every row literal.
|
|
406
|
+
if (groupOffsets.length === 0) {
|
|
407
|
+
template += " export const List: Record[] = [\n";
|
|
408
|
+
for (let i = 0; i < rowStrings.length; i++) {
|
|
409
|
+
template += rowStrings[i];
|
|
410
|
+
if (i < rowStrings.length - 1) {
|
|
411
|
+
template += ",";
|
|
412
|
+
}
|
|
413
|
+
template += "\n";
|
|
340
414
|
}
|
|
341
|
-
template += "\n";
|
|
415
|
+
template += " ];\n\n";
|
|
416
|
+
}
|
|
417
|
+
if (groupOffsets.length > 0) {
|
|
418
|
+
// The FileMap keys are a literal union of the member file names, so a
|
|
419
|
+
// RefTable cell that names a file which does not exist fails at tsc
|
|
420
|
+
// compile time (FileMap["ch99"] is not indexable) instead of silently
|
|
421
|
+
// producing undefined at runtime. A mapped type is used instead of
|
|
422
|
+
// `Record<File, ...>` because the namespace's own `Record` interface
|
|
423
|
+
// shadows the global utility type; it also rejects a missing group.
|
|
424
|
+
const fileNames = groupOffsets.map((group) => JSON.stringify(group.name));
|
|
425
|
+
template += ` export type File = ${fileNames.join(" | ")};\n`;
|
|
426
|
+
template += ` export const FileList: File[] = [${fileNames.join(", ")}];\n\n`;
|
|
427
|
+
template += " export const FileMap: { [file in File]: Record[] } = {\n";
|
|
428
|
+
for (const group of groupOffsets) {
|
|
429
|
+
template += ` ${serializeField(group.name)}: [\n`;
|
|
430
|
+
for (let i = group.start; i < group.end; i++) {
|
|
431
|
+
template += rowStrings[i] + ",\n";
|
|
432
|
+
}
|
|
433
|
+
template += " ],\n";
|
|
434
|
+
}
|
|
435
|
+
template += " };\n\n";
|
|
436
|
+
// RefTable resolves through this accessor instead of indexing FileMap
|
|
437
|
+
// directly: indexing an unknown literal key is only an error under
|
|
438
|
+
// noImplicitAny, while passing a wrong argument to a `File` parameter is
|
|
439
|
+
// an error under every tsc flag combination.
|
|
440
|
+
template += " export function refTable(file: File): Record[] { return FileMap[file]; }\n\n";
|
|
342
441
|
}
|
|
343
|
-
template += " ];\n\n";
|
|
344
442
|
if (indexField != null) {
|
|
345
443
|
template += " export const Map: { [id: string]: Record } = {};\n";
|
|
346
|
-
|
|
444
|
+
if (groupOffsets.length > 0) {
|
|
445
|
+
template += ` for (const file in FileMap) { for (const v of FileMap[file]) { Map[v.${indexField}] = v; } };\n\n`;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
template += ` for (const v of List) { Map[v.${indexField}] = v; };\n\n`;
|
|
449
|
+
}
|
|
347
450
|
}
|
|
348
451
|
template += "};";
|
|
349
452
|
return template;
|
|
@@ -441,6 +544,9 @@ function isKnownType(typeStr) {
|
|
|
441
544
|
if (parseRefEnum(typeStr)) {
|
|
442
545
|
return true;
|
|
443
546
|
}
|
|
547
|
+
if (parseRefTable(typeStr)) {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
444
550
|
if (parseTemplate(typeStr)) {
|
|
445
551
|
return true;
|
|
446
552
|
}
|
|
@@ -465,6 +571,10 @@ function isStructuralType(typeStr) {
|
|
|
465
571
|
if (refEnum && refEnum.isArray) {
|
|
466
572
|
return true;
|
|
467
573
|
}
|
|
574
|
+
const refTable = parseRefTable(typeStr);
|
|
575
|
+
if (refTable && refTable.isArray) {
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
468
578
|
if (parseTemplate(typeStr)) {
|
|
469
579
|
return true;
|
|
470
580
|
}
|
|
@@ -510,6 +620,29 @@ function parseRefEnum(typeStr) {
|
|
|
510
620
|
}
|
|
511
621
|
return null;
|
|
512
622
|
}
|
|
623
|
+
// Parse RefTable[module] (single) and RefTable[module][] (array of sub-table
|
|
624
|
+
// references). A RefTable cell holds a sub-table file name of a merged
|
|
625
|
+
// subdirectory unit (the exported FileMap key) and resolves to that file's
|
|
626
|
+
// row list. The prefixes are matched in full, so "RefTable[" never collides
|
|
627
|
+
// with "Ref[" / "RefEnum[".
|
|
628
|
+
function parseRefTable(typeStr) {
|
|
629
|
+
if (!typeStr.startsWith(RefTablePrefix)) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
let isArray = false;
|
|
633
|
+
let inner = typeStr;
|
|
634
|
+
if (typeStr.endsWith(RefEnumArraySuffix)) {
|
|
635
|
+
isArray = true;
|
|
636
|
+
inner = typeStr.slice(0, -RefEnumArraySuffix.length) + RefSuffix;
|
|
637
|
+
}
|
|
638
|
+
if (inner.startsWith(RefTablePrefix) && inner.endsWith(RefSuffix)) {
|
|
639
|
+
const refModule = inner.slice(RefTablePrefix.length, -RefSuffix.length);
|
|
640
|
+
if (refModule !== "") {
|
|
641
|
+
return { refModule, isArray };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
513
646
|
// Parse Template[module] (single) and Template[module][] (array of templated
|
|
514
647
|
// row references). "Template[" starts with 'T', so it never collides with the
|
|
515
648
|
// "Ref[" / "RefEnum[" prefixes.
|
|
@@ -636,6 +769,13 @@ function GetFileExt(filePath) {
|
|
|
636
769
|
const pathObject = path.parse(filePath);
|
|
637
770
|
return pathObject.ext.slice(1);
|
|
638
771
|
}
|
|
772
|
+
// Read a config file and strip the UTF-8 BOM Excel likes to add.
|
|
773
|
+
function readConfigFile(filePath) {
|
|
774
|
+
let fileString = fs.readFileSync(filePath).toString();
|
|
775
|
+
if (fileString.charAt(0) === "\uFEFF")
|
|
776
|
+
fileString = fileString.substr(1);
|
|
777
|
+
return fileString;
|
|
778
|
+
}
|
|
639
779
|
function scanDir(dir, basePath) {
|
|
640
780
|
const node = {};
|
|
641
781
|
let hasContent = false;
|
|
@@ -754,14 +894,10 @@ function assets2ts(assetsDir) {
|
|
|
754
894
|
return template;
|
|
755
895
|
}
|
|
756
896
|
function GetTsString(filePath) {
|
|
757
|
-
const pathObject = path.parse(filePath);
|
|
758
897
|
const handle = exports.Convert[GetFileExt(filePath)];
|
|
759
898
|
if (handle) {
|
|
760
|
-
const moduleName = changeCase.pascalCase(
|
|
761
|
-
|
|
762
|
-
if (fileString.charAt(0) === "\uFEFF")
|
|
763
|
-
fileString = fileString.substr(1);
|
|
764
|
-
return handle(fileString, moduleName);
|
|
899
|
+
const moduleName = changeCase.pascalCase(path.parse(filePath).base);
|
|
900
|
+
return handle(readConfigFile(filePath), moduleName);
|
|
765
901
|
}
|
|
766
902
|
else {
|
|
767
903
|
return "";
|
|
@@ -770,23 +906,17 @@ function GetTsString(filePath) {
|
|
|
770
906
|
// Collect the config file names a CSV references from its type row
|
|
771
907
|
// (Ref[file], RefEnum[file.field], Template[file] and their array forms).
|
|
772
908
|
// ini/toml files never reference other config tables.
|
|
773
|
-
function
|
|
774
|
-
if (GetFileExt(filePath) !== "csv") {
|
|
775
|
-
return [];
|
|
776
|
-
}
|
|
777
|
-
let fileString = fs.readFileSync(filePath).toString();
|
|
778
|
-
if (fileString.charAt(0) === "\uFEFF") {
|
|
779
|
-
fileString = fileString.substr(1);
|
|
780
|
-
}
|
|
909
|
+
function collectCsvReferencesFromContent(csvString) {
|
|
781
910
|
const references = [];
|
|
782
|
-
(0, d3_dsv_1.csvParse)(
|
|
911
|
+
(0, d3_dsv_1.csvParse)(csvString, function (d, i) {
|
|
783
912
|
if (i === 0) {
|
|
784
913
|
for (const k in d) {
|
|
785
914
|
const typeStr = d[k];
|
|
786
915
|
const ref = parseRef(typeStr);
|
|
787
916
|
const refEnum = parseRefEnum(typeStr);
|
|
917
|
+
const refTable = parseRefTable(typeStr);
|
|
788
918
|
const template = parseTemplate(typeStr);
|
|
789
|
-
const target = ref ? ref.refModule : refEnum ? refEnum.refModule : template ? template.refModule : null;
|
|
919
|
+
const target = ref ? ref.refModule : refEnum ? refEnum.refModule : refTable ? refTable.refModule : template ? template.refModule : null;
|
|
790
920
|
if (target && !references.includes(target)) {
|
|
791
921
|
references.push(target);
|
|
792
922
|
}
|
|
@@ -796,31 +926,263 @@ function collectCsvReferences(filePath) {
|
|
|
796
926
|
});
|
|
797
927
|
return references;
|
|
798
928
|
}
|
|
799
|
-
//
|
|
929
|
+
// Resolve a table reference to a full unit name. References are written
|
|
930
|
+
// without the extension by default ("Ref[skill]" -> "skill.csv"), matching
|
|
931
|
+
// how table names read in generated code (pascalCase("skill.csv") ===
|
|
932
|
+
// "SkillCsv"). Exact unit names still win, so legacy refs that spell out the
|
|
933
|
+
// extension ("Ref[skill.csv]") keep working; a stem shared by several units
|
|
934
|
+
// (e.g. skill.csv + skill.ini) is ambiguous and reported.
|
|
935
|
+
function resolveTableName(ref, names) {
|
|
936
|
+
const nameList = Array.from(names);
|
|
937
|
+
if (nameList.includes(ref)) {
|
|
938
|
+
return ref;
|
|
939
|
+
}
|
|
940
|
+
const stemMatches = nameList.filter((name) => path.parse(name).name === ref);
|
|
941
|
+
if (stemMatches.length === 1) {
|
|
942
|
+
return stemMatches[0];
|
|
943
|
+
}
|
|
944
|
+
if (stemMatches.length > 1) {
|
|
945
|
+
console.warn(`[config2ts] warning: table reference "${ref}" is ambiguous (${stemMatches.join(", ")}), write the full file name in the ref`);
|
|
946
|
+
}
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
// Rewrite the type row's table references from stem form to full unit names,
|
|
950
|
+
// so downstream steps (namespace derivation, import lines, dependency
|
|
951
|
+
// sorting) keep matching refs against unit names exactly. The content is only
|
|
952
|
+
// re-serialized when something actually changed.
|
|
953
|
+
function normalizeCsvRefs(content, allNames) {
|
|
954
|
+
if (allNames.size === 0) {
|
|
955
|
+
return content;
|
|
956
|
+
}
|
|
957
|
+
const rows = (0, d3_dsv_1.csvParseRows)(content);
|
|
958
|
+
if (rows.length < 2) {
|
|
959
|
+
return content;
|
|
960
|
+
}
|
|
961
|
+
let changed = false;
|
|
962
|
+
rows[1] = rows[1].map((cell) => {
|
|
963
|
+
const ref = parseRef(cell);
|
|
964
|
+
if (ref) {
|
|
965
|
+
const resolved = resolveTableName(ref.refModule, allNames);
|
|
966
|
+
if (resolved) {
|
|
967
|
+
changed = true;
|
|
968
|
+
return `Ref[${resolved}]` + (ref.isArray ? "[]" : "");
|
|
969
|
+
}
|
|
970
|
+
return cell;
|
|
971
|
+
}
|
|
972
|
+
const refEnum = parseRefEnum(cell);
|
|
973
|
+
if (refEnum) {
|
|
974
|
+
const resolved = resolveTableName(refEnum.refModule, allNames);
|
|
975
|
+
if (resolved) {
|
|
976
|
+
changed = true;
|
|
977
|
+
return `RefEnum[${resolved}.${refEnum.refField}]` + (refEnum.isArray ? "[]" : "");
|
|
978
|
+
}
|
|
979
|
+
return cell;
|
|
980
|
+
}
|
|
981
|
+
const refTable = parseRefTable(cell);
|
|
982
|
+
if (refTable) {
|
|
983
|
+
const resolved = resolveTableName(refTable.refModule, allNames);
|
|
984
|
+
if (resolved) {
|
|
985
|
+
changed = true;
|
|
986
|
+
return `RefTable[${resolved}]` + (refTable.isArray ? "[]" : "");
|
|
987
|
+
}
|
|
988
|
+
return cell;
|
|
989
|
+
}
|
|
990
|
+
const template = parseTemplate(cell);
|
|
991
|
+
if (template) {
|
|
992
|
+
const resolved = resolveTableName(template.refModule, allNames);
|
|
993
|
+
if (resolved) {
|
|
994
|
+
changed = true;
|
|
995
|
+
return `Template[${resolved}]` + (template.isArray ? "[]" : "");
|
|
996
|
+
}
|
|
997
|
+
return cell;
|
|
998
|
+
}
|
|
999
|
+
return cell;
|
|
1000
|
+
});
|
|
1001
|
+
return changed ? (0, d3_dsv_1.csvFormatRows)(rows) : content;
|
|
1002
|
+
}
|
|
1003
|
+
function makeFileUnit(filePath, allNames) {
|
|
1004
|
+
const name = path.basename(filePath);
|
|
1005
|
+
const ext = GetFileExt(name);
|
|
1006
|
+
const content = readConfigFile(filePath);
|
|
1007
|
+
const normalized = ext === "csv" && allNames ? normalizeCsvRefs(content, allNames) : content;
|
|
1008
|
+
return {
|
|
1009
|
+
name,
|
|
1010
|
+
ext,
|
|
1011
|
+
content: normalized,
|
|
1012
|
+
sourcePaths: [filePath],
|
|
1013
|
+
refs: ext === "csv" ? collectCsvReferencesFromContent(normalized) : [],
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
// A subdirectory whose convertible files are ALL csv and share identical
|
|
1017
|
+
// header + type rows is merged into one virtual table named after the
|
|
1018
|
+
// directory ("<dir>.csv" -> pascalCase -> "<Dir>Csv"). Data rows are
|
|
1019
|
+
// concatenated in file-name order, so per-chapter/per-language splits stay
|
|
1020
|
+
// manageable on disk while the runtime sees a single table.
|
|
1021
|
+
function tryMergeSubdir(dir, dirName, allNames) {
|
|
1022
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile());
|
|
1023
|
+
const files = entries
|
|
1024
|
+
.map((e) => path.join(dir, e.name))
|
|
1025
|
+
.filter((f) => exports.Convert[GetFileExt(f)] != null)
|
|
1026
|
+
.sort();
|
|
1027
|
+
if (files.length === 0) {
|
|
1028
|
+
console.warn(`[config2ts] warning: directory "${dirName}" contains no convertible config files, skipped`);
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
const nonCsv = files.filter((f) => GetFileExt(f) !== "csv");
|
|
1032
|
+
if (nonCsv.length > 0) {
|
|
1033
|
+
console.warn(`[config2ts] warning: directory "${dirName}" mixes non-csv files (${nonCsv.map((f) => path.basename(f)).join(", ")}), cannot merge into one table, skipped`);
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
const contents = files.map(readConfigFile);
|
|
1037
|
+
const headRows = contents.map((c) => (0, d3_dsv_1.csvParseRows)(c).slice(0, 2));
|
|
1038
|
+
const headKey = JSON.stringify(headRows[0]);
|
|
1039
|
+
const uniform = headRows.every((rows) => rows.length === 2 && JSON.stringify(rows) === headKey);
|
|
1040
|
+
if (!uniform) {
|
|
1041
|
+
console.warn(`[config2ts] warning: directory "${dirName}" contains csv files with different header/type rows, cannot merge into one table, skipped`);
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
// Keep the first file's header + type rows once; every following file
|
|
1045
|
+
// contributes its data rows only.
|
|
1046
|
+
const allRows = [
|
|
1047
|
+
...(0, d3_dsv_1.csvParseRows)(contents[0]),
|
|
1048
|
+
...contents.slice(1).flatMap((c) => (0, d3_dsv_1.csvParseRows)(c).slice(2)),
|
|
1049
|
+
];
|
|
1050
|
+
const mergedContent = (0, d3_dsv_1.csvFormatRows)(allRows);
|
|
1051
|
+
const keyType = headRows[0][1][0];
|
|
1052
|
+
if (keyType === IndexStr || keyType === EnumIndexStr) {
|
|
1053
|
+
const seen = new Set();
|
|
1054
|
+
for (let i = 2; i < allRows.length; i++) {
|
|
1055
|
+
const key = allRows[i][0];
|
|
1056
|
+
if (key !== "" && seen.has(key)) {
|
|
1057
|
+
console.warn(`[config2ts] warning: merged table "${dirName}" has duplicate ${keyType} key "${key}"`);
|
|
1058
|
+
}
|
|
1059
|
+
seen.add(key);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
const name = dirName.endsWith(".csv") ? dirName : dirName + ".csv";
|
|
1063
|
+
const subTables = files.map((f, i) => ({
|
|
1064
|
+
name: path.parse(f).name,
|
|
1065
|
+
rowCount: Math.max(0, (0, d3_dsv_1.csvParseRows)(contents[i]).length - 2),
|
|
1066
|
+
}));
|
|
1067
|
+
const mergedRefsContent = allNames ? normalizeCsvRefs(mergedContent, allNames) : mergedContent;
|
|
1068
|
+
return {
|
|
1069
|
+
name,
|
|
1070
|
+
ext: "csv",
|
|
1071
|
+
content: mergedRefsContent,
|
|
1072
|
+
sourcePaths: files,
|
|
1073
|
+
refs: collectCsvReferencesFromContent(mergedRefsContent),
|
|
1074
|
+
subTables,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
// RefTable cells must name an existing member file of the target merged
|
|
1078
|
+
// table. The generated FileMap is keyed by a literal union, so a wrong name
|
|
1079
|
+
// also fails tsc; checking here surfaces the mistake at conversion time with
|
|
1080
|
+
// the valid keys listed, and without needing a compile step.
|
|
1081
|
+
function validateRefTableValues(units) {
|
|
1082
|
+
var _a;
|
|
1083
|
+
const unitByName = new Map();
|
|
1084
|
+
for (const unit of units) {
|
|
1085
|
+
unitByName.set(unit.name, unit);
|
|
1086
|
+
}
|
|
1087
|
+
for (const unit of units) {
|
|
1088
|
+
if (unit.ext !== "csv")
|
|
1089
|
+
continue;
|
|
1090
|
+
const rows = (0, d3_dsv_1.csvParseRows)(unit.content);
|
|
1091
|
+
if (rows.length < 2)
|
|
1092
|
+
continue;
|
|
1093
|
+
const headerRow = rows[0];
|
|
1094
|
+
const typeRow = rows[1];
|
|
1095
|
+
for (let column = 0; column < typeRow.length; column++) {
|
|
1096
|
+
const refTable = parseRefTable(typeRow[column]);
|
|
1097
|
+
if (!refTable)
|
|
1098
|
+
continue;
|
|
1099
|
+
const target = unitByName.get(refTable.refModule);
|
|
1100
|
+
if (!target)
|
|
1101
|
+
continue;
|
|
1102
|
+
if (!target.subTables) {
|
|
1103
|
+
// The target table exists but is a plain file, so it has no FileMap /
|
|
1104
|
+
// refTable() at all — the generated call would fail tsc with a
|
|
1105
|
+
// confusing "property does not exist" error, so say it plainly here.
|
|
1106
|
+
console.warn(`[config2ts] warning: ${unit.name} field "${headerRow[column]}" uses RefTable[${refTable.refModule}], but "${target.name}" is not a merged subdirectory table (it has no sub-tables)`);
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
const validKeys = new Set(target.subTables.map((subTable) => subTable.name));
|
|
1110
|
+
for (let rowIndex = 2; rowIndex < rows.length; rowIndex++) {
|
|
1111
|
+
const cell = normalizeCell((_a = rows[rowIndex][column]) !== null && _a !== void 0 ? _a : "");
|
|
1112
|
+
const names = refTable.isArray ? splitStringArray(cell) : cell === "" ? [] : [cell];
|
|
1113
|
+
for (const name of names) {
|
|
1114
|
+
if (name !== "" && !validKeys.has(name)) {
|
|
1115
|
+
// Row numbers follow the other csv warnings: the type row is 0,
|
|
1116
|
+
// so the first data row (csvParseRows index 2) is row 1.
|
|
1117
|
+
console.warn(`[config2ts] warning: ${unit.name} row ${rowIndex - 1} field "${headerRow[column]}" references sub-table "${name}" which does not exist in "${target.name}" (valid: ${Array.from(validKeys).join(", ")})`);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
// Resolve a config directory into convert units: every convertible file
|
|
1125
|
+
// becomes one unit; every subdirectory is merged into a single unit when its
|
|
1126
|
+
// csv files are homogeneous (warned and skipped otherwise). Unit names are
|
|
1127
|
+
// predicted first, so extension-less table refs in the type rows can be
|
|
1128
|
+
// resolved before dependency collection.
|
|
1129
|
+
function collectConvertUnits(dir) {
|
|
1130
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
1131
|
+
const allNames = new Set();
|
|
1132
|
+
for (const entry of entries) {
|
|
1133
|
+
if (entry.isFile()) {
|
|
1134
|
+
if (exports.Convert[GetFileExt(entry.name)] == null)
|
|
1135
|
+
continue;
|
|
1136
|
+
allNames.add(entry.name);
|
|
1137
|
+
}
|
|
1138
|
+
else if (entry.isDirectory()) {
|
|
1139
|
+
allNames.add(entry.name.endsWith(".csv") ? entry.name : entry.name + ".csv");
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const units = [];
|
|
1143
|
+
const takenNames = new Set();
|
|
1144
|
+
const take = function (unit) {
|
|
1145
|
+
if (takenNames.has(unit.name)) {
|
|
1146
|
+
console.warn(`[config2ts] warning: duplicate table name "${unit.name}", the generated namespaces will collide`);
|
|
1147
|
+
}
|
|
1148
|
+
takenNames.add(unit.name);
|
|
1149
|
+
units.push(unit);
|
|
1150
|
+
};
|
|
1151
|
+
for (const entry of entries) {
|
|
1152
|
+
const full = path.join(dir, entry.name);
|
|
1153
|
+
if (entry.isFile()) {
|
|
1154
|
+
if (exports.Convert[GetFileExt(entry.name)] == null)
|
|
1155
|
+
continue;
|
|
1156
|
+
take(makeFileUnit(full, allNames));
|
|
1157
|
+
}
|
|
1158
|
+
else if (entry.isDirectory()) {
|
|
1159
|
+
const merged = tryMergeSubdir(full, entry.name, allNames);
|
|
1160
|
+
if (merged)
|
|
1161
|
+
take(merged);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
validateRefTableValues(units);
|
|
1165
|
+
return units;
|
|
1166
|
+
}
|
|
1167
|
+
// Order units so every referenced table is emitted before the table that
|
|
800
1168
|
// references it (a referenced namespace must exist when the referencing
|
|
801
1169
|
// table's data initializes), so authors no longer need filename tricks (e.g.
|
|
802
|
-
// an "!" prefix) to control order.
|
|
803
|
-
// their alphabetical order. References to missing
|
|
1170
|
+
// an "!" prefix) to control order. Units without a dependency relation keep
|
|
1171
|
+
// their alphabetical order. References to missing tables and circular
|
|
804
1172
|
// references are reported; a cycle is broken at the back edge so output is
|
|
805
1173
|
// still generated (the runtime data still needs the cycle fixed by hand).
|
|
806
|
-
function
|
|
807
|
-
const ordered =
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
});
|
|
812
|
-
const fileByBase = new Map();
|
|
813
|
-
for (const filePath of ordered) {
|
|
814
|
-
fileByBase.set(path.basename(filePath), filePath);
|
|
1174
|
+
function sortUnitsByReferences(units) {
|
|
1175
|
+
const ordered = units.slice().sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
1176
|
+
const unitByName = new Map();
|
|
1177
|
+
for (const unit of ordered) {
|
|
1178
|
+
unitByName.set(unit.name, unit);
|
|
815
1179
|
}
|
|
816
|
-
const
|
|
817
|
-
for (const
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
if (!fileByBase.has(dep)) {
|
|
823
|
-
console.warn(`[config2ts] warning: ${base} references "${dep}" but no config file with that name was found in the config directory`);
|
|
1180
|
+
const depsByName = new Map();
|
|
1181
|
+
for (const unit of ordered) {
|
|
1182
|
+
depsByName.set(unit.name, unit.refs);
|
|
1183
|
+
for (const dep of unit.refs) {
|
|
1184
|
+
if (!unitByName.has(dep)) {
|
|
1185
|
+
console.warn(`[config2ts] warning: ${unit.name} references "${dep}" but no config file with that name was found in the config directory`);
|
|
824
1186
|
}
|
|
825
1187
|
}
|
|
826
1188
|
}
|
|
@@ -828,57 +1190,133 @@ function sortFilesByReferences(fileList) {
|
|
|
828
1190
|
// 0 = unvisited, 1 = visiting (on the current DFS trail), 2 = done
|
|
829
1191
|
const state = new Map();
|
|
830
1192
|
const reportedCycles = new Set();
|
|
831
|
-
function visit(
|
|
1193
|
+
function visit(name, trail) {
|
|
832
1194
|
var _a, _b;
|
|
833
|
-
const current = (_a = state.get(
|
|
1195
|
+
const current = (_a = state.get(name)) !== null && _a !== void 0 ? _a : 0;
|
|
834
1196
|
if (current === 2) {
|
|
835
1197
|
return;
|
|
836
1198
|
}
|
|
837
1199
|
if (current === 1) {
|
|
838
|
-
const cycleStart = trail.indexOf(
|
|
839
|
-
const cycleKey = trail.slice(cycleStart).concat(
|
|
1200
|
+
const cycleStart = trail.indexOf(name);
|
|
1201
|
+
const cycleKey = trail.slice(cycleStart).concat(name).join(" -> ");
|
|
840
1202
|
if (!reportedCycles.has(cycleKey)) {
|
|
841
1203
|
reportedCycles.add(cycleKey);
|
|
842
1204
|
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`);
|
|
843
1205
|
}
|
|
844
1206
|
return;
|
|
845
1207
|
}
|
|
846
|
-
state.set(
|
|
847
|
-
trail.push(
|
|
848
|
-
for (const dep of (_b =
|
|
849
|
-
if (
|
|
1208
|
+
state.set(name, 1);
|
|
1209
|
+
trail.push(name);
|
|
1210
|
+
for (const dep of (_b = depsByName.get(name)) !== null && _b !== void 0 ? _b : []) {
|
|
1211
|
+
if (unitByName.has(dep)) {
|
|
850
1212
|
visit(dep, trail);
|
|
851
1213
|
}
|
|
852
1214
|
}
|
|
853
1215
|
trail.pop();
|
|
854
|
-
state.set(
|
|
855
|
-
sorted.push(
|
|
1216
|
+
state.set(name, 2);
|
|
1217
|
+
sorted.push(unitByName.get(name));
|
|
856
1218
|
}
|
|
857
|
-
for (const
|
|
858
|
-
visit(
|
|
1219
|
+
for (const unit of ordered) {
|
|
1220
|
+
visit(unit.name, []);
|
|
859
1221
|
}
|
|
860
1222
|
return sorted;
|
|
861
1223
|
}
|
|
862
|
-
function
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1224
|
+
function renderUnit(unit) {
|
|
1225
|
+
const moduleName = changeCase.pascalCase(unit.name);
|
|
1226
|
+
// Merged subdirectories render through the sub-table-aware path so the
|
|
1227
|
+
// generated namespace also exposes FileMap (file name -> rows).
|
|
1228
|
+
if (unit.ext === "csv" && unit.subTables && unit.subTables.length > 0) {
|
|
1229
|
+
return csv2ts(unit.content, moduleName, unit.subTables);
|
|
1230
|
+
}
|
|
1231
|
+
const handle = exports.Convert[unit.ext];
|
|
1232
|
+
if (!handle) {
|
|
1233
|
+
return "";
|
|
1234
|
+
}
|
|
1235
|
+
return handle(unit.content, moduleName);
|
|
1236
|
+
}
|
|
1237
|
+
function GetTsStringFromUnits(units) {
|
|
1238
|
+
return sortUnitsByReferences(units)
|
|
1239
|
+
.map(renderUnit)
|
|
867
1240
|
.join("\n\n");
|
|
868
1241
|
}
|
|
1242
|
+
function GetTsStringFromFileList(fileList) {
|
|
1243
|
+
const allNames = new Set(fileList.map((filePath) => path.basename(filePath)));
|
|
1244
|
+
return GetTsStringFromUnits(fileList.map((filePath) => makeFileUnit(filePath, allNames)));
|
|
1245
|
+
}
|
|
869
1246
|
function GetValidFileList(fileList) {
|
|
870
1247
|
return fileList.filter(function (filePath) {
|
|
871
1248
|
return exports.Convert[GetFileExt(filePath)] != null;
|
|
872
1249
|
});
|
|
873
1250
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1251
|
+
// Build the `import { XxxCsv } from "./xxx";` lines a split-mode file needs so
|
|
1252
|
+
// its cross-table Ref/RefEnum/Template references resolve. `unit.refs` holds the
|
|
1253
|
+
// full base names (e.g. "z-base.csv") of referenced tables, which map 1:1 to
|
|
1254
|
+
// the split output file "./z-base.ts" and to the namespace
|
|
1255
|
+
// `pascalCase("z-base.csv")` === "ZBaseCsv".
|
|
1256
|
+
function buildImportLines(unit, validNames) {
|
|
1257
|
+
const refs = unit.refs.filter((ref) => ref !== unit.name);
|
|
1258
|
+
const imported = new Set();
|
|
1259
|
+
const lines = [];
|
|
1260
|
+
for (const ref of refs) {
|
|
1261
|
+
if (!validNames.has(ref)) {
|
|
1262
|
+
continue; // missing referenced table; already warned by sortUnitsByReferences
|
|
1263
|
+
}
|
|
1264
|
+
if (imported.has(ref)) {
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
imported.add(ref);
|
|
1268
|
+
const ns = changeCase.pascalCase(ref);
|
|
1269
|
+
const importPath = "./" + path.parse(ref).name;
|
|
1270
|
+
lines.push(`import { ${ns} } from "${importPath}";`);
|
|
1271
|
+
}
|
|
1272
|
+
return lines.join("\n");
|
|
1273
|
+
}
|
|
1274
|
+
function startConvert(dir, outDir, merge, assetsDir, mode = "single") {
|
|
1275
|
+
const units = collectConvertUnits(dir);
|
|
1276
|
+
const fileCount = units.reduce((n, u) => n + u.sourcePaths.length, 0);
|
|
1277
|
+
if (mode === "split") {
|
|
1278
|
+
const validNames = new Set(units.map((u) => u.name));
|
|
1279
|
+
const orderedUnits = sortUnitsByReferences(units);
|
|
1280
|
+
const exportedNames = [];
|
|
1281
|
+
for (const unit of orderedUnits) {
|
|
1282
|
+
const outName = path.parse(unit.name).name + ".ts";
|
|
1283
|
+
exportedNames.push(path.parse(unit.name).name);
|
|
1284
|
+
const imports = buildImportLines(unit, validNames);
|
|
1285
|
+
const body = renderUnit(unit);
|
|
1286
|
+
const content = AutoGenHeader + (imports ? imports + "\n\n" : "") + body + "\n";
|
|
1287
|
+
const outPath = path.join(outDir, outName);
|
|
1288
|
+
fs.writeFileSync(outPath, content, { encoding: "utf-8" });
|
|
1289
|
+
const source = unit.sourcePaths.length > 1 ? `${unit.name} (${unit.sourcePaths.length} files)` : path.basename(unit.sourcePaths[0]);
|
|
1290
|
+
console.log(`config2ts, ${source} -> ${outPath}`);
|
|
1291
|
+
}
|
|
1292
|
+
let assetsExport = null;
|
|
1293
|
+
if (assetsDir) {
|
|
1294
|
+
const assetsOutput = assets2ts(assetsDir);
|
|
1295
|
+
if (assetsOutput) {
|
|
1296
|
+
const assetsFile = path.join(outDir, "assets.ts");
|
|
1297
|
+
fs.writeFileSync(assetsFile, AutoGenHeader + assetsOutput, { encoding: "utf-8" });
|
|
1298
|
+
console.log(`assets2ts, resource dir: ${assetsDir}, output: ${assetsFile}`);
|
|
1299
|
+
assetsExport = "assets";
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
// In split mode the `-n` value is reused as the index file name; fall back to
|
|
1303
|
+
// "index.ts" when the caller leaves the single-mode default "csv.ts" intact.
|
|
1304
|
+
const indexName = merge !== "csv.ts" ? merge : "index.ts";
|
|
1305
|
+
let indexContent = AutoGenHeader;
|
|
1306
|
+
for (const name of exportedNames) {
|
|
1307
|
+
indexContent += `export * from "./${name}";\n`;
|
|
1308
|
+
}
|
|
1309
|
+
if (assetsExport) {
|
|
1310
|
+
indexContent += `export * from "./${assetsExport}";\n`;
|
|
1311
|
+
}
|
|
1312
|
+
const indexPath = path.join(outDir, indexName);
|
|
1313
|
+
fs.writeFileSync(indexPath, indexContent, { encoding: "utf-8" });
|
|
1314
|
+
console.log(`config2ts, ${orderedUnits.length} config tables (${fileCount} files), split mode, index: ${indexPath}`);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
879
1317
|
const mergeFile = path.join(outDir, merge);
|
|
880
|
-
fs.writeFileSync(mergeFile, AutoGenHeader +
|
|
881
|
-
console.log(`config2ts, ${
|
|
1318
|
+
fs.writeFileSync(mergeFile, AutoGenHeader + GetTsStringFromUnits(units), { encoding: "utf-8" });
|
|
1319
|
+
console.log(`config2ts, ${units.length} config tables (${fileCount} files), merge into: ${mergeFile}`);
|
|
882
1320
|
if (assetsDir) {
|
|
883
1321
|
const assetsOutput = assets2ts(assetsDir);
|
|
884
1322
|
if (assetsOutput) {
|
package/lib/main.js
CHANGED
|
@@ -42,10 +42,11 @@ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
|
42
42
|
const program = new commander_1.Command();
|
|
43
43
|
program
|
|
44
44
|
.version(pkg.version)
|
|
45
|
-
.option("-n, --name <name>", "output file name", "csv.ts")
|
|
45
|
+
.option("-n, --name <name>", "output file name (single mode) / index file name (split mode)", "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
|
|
48
|
+
.option("-a, --assets <path>", "set assets resource directory for asset index (optional, skip asset index if omitted)")
|
|
49
|
+
.option("-m, --mode <mode>", "output mode: single (merge into one file) or split (one file per table + index.ts)", "single")
|
|
49
50
|
.parse(process.argv);
|
|
50
51
|
const options = program.opts();
|
|
51
52
|
console.log(options);
|
|
@@ -60,4 +61,8 @@ const assetsDir = options.assets ? path.resolve(options.assets) : undefined;
|
|
|
60
61
|
if (assetsDir) {
|
|
61
62
|
console.log("assetsDir:", assetsDir);
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
+
const mode = options.mode === "split" ? "split" : "single";
|
|
65
|
+
if (mode === "single" && options.mode !== "single") {
|
|
66
|
+
console.warn(`[config2ts] warning: unknown mode "${options.mode}", falling back to "single"`);
|
|
67
|
+
}
|
|
68
|
+
(0, config2ts_1.startConvert)(dir, outDir, options.name, assetsDir, mode);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "config2ts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.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",
|
|
@@ -13,9 +13,12 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
16
|
+
"fixtures:regen": "npm run build && node ./scripts/regen-fixtures.js",
|
|
17
|
+
"demo:dev": "npm --prefix website run dev",
|
|
18
|
+
"demo:build": "npm --prefix website run build",
|
|
19
|
+
"pretest": "node -e \"require('fs').rmSync('.test-build',{recursive:true,force:true})\"",
|
|
20
|
+
"test": "npm run build && tsc -p tsconfig.test.json && node --test \".test-build/tests/*.test.js\"",
|
|
21
|
+
"test:coverage": "npm run build && tsc -p tsconfig.test.json && node --test --experimental-test-coverage \".test-build/tests/*.test.js\""
|
|
19
22
|
},
|
|
20
23
|
"repository": {
|
|
21
24
|
"type": "git",
|
|
@@ -39,19 +42,7 @@
|
|
|
39
42
|
"toml": "^4.1.2"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|
|
42
|
-
"@types/jest": "^30.0.0",
|
|
43
45
|
"@types/node": "^24.5.2",
|
|
44
|
-
"jest": "^30.5.1",
|
|
45
|
-
"ts-jest": "^29.4.12",
|
|
46
46
|
"typescript": "^5.9.2"
|
|
47
|
-
},
|
|
48
|
-
"overrides": {
|
|
49
|
-
"@babel/core": "^7.29.6",
|
|
50
|
-
"browserslist": "^4.28.7",
|
|
51
|
-
"handlebars": "^4.7.9",
|
|
52
|
-
"js-yaml": "^3.14.2",
|
|
53
|
-
"minimatch@^3.0.0": "^3.1.3",
|
|
54
|
-
"picomatch@^2.0.0": "^2.3.2",
|
|
55
|
-
"brace-expansion@^1.0.0": "^1.1.16"
|
|
56
47
|
}
|
|
57
48
|
}
|