@amxts/core 0.1.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/LICENSE +21 -0
- package/README.md +162 -0
- package/README.ru.md +162 -0
- package/amxmodx/base/include/amxconst.inc +594 -0
- package/amxmodx/base/include/amxmisc.inc +902 -0
- package/amxmodx/base/include/amxmodx.inc +3447 -0
- package/amxmodx/base/include/amxmodx_version.inc +20 -0
- package/amxmodx/base/include/cellarray.inc +525 -0
- package/amxmodx/base/include/cellstack.inc +166 -0
- package/amxmodx/base/include/celltrie.inc +412 -0
- package/amxmodx/base/include/core.inc +202 -0
- package/amxmodx/base/include/csstats.inc +298 -0
- package/amxmodx/base/include/csstats_const.inc +29 -0
- package/amxmodx/base/include/cstrike.inc +1287 -0
- package/amxmodx/base/include/cstrike_const.inc +521 -0
- package/amxmodx/base/include/csx.inc +259 -0
- package/amxmodx/base/include/cvars.inc +547 -0
- package/amxmodx/base/include/datapack.inc +161 -0
- package/amxmodx/base/include/dbi.inc +154 -0
- package/amxmodx/base/include/dodconst.inc +152 -0
- package/amxmodx/base/include/dodfun.inc +152 -0
- package/amxmodx/base/include/dodstats.inc +69 -0
- package/amxmodx/base/include/dodx.inc +161 -0
- package/amxmodx/base/include/engine.inc +1343 -0
- package/amxmodx/base/include/engine_const.inc +310 -0
- package/amxmodx/base/include/engine_stocks.inc +248 -0
- package/amxmodx/base/include/esf.inc +70 -0
- package/amxmodx/base/include/esf_const.inc +74 -0
- package/amxmodx/base/include/fakemeta.inc +1127 -0
- package/amxmodx/base/include/fakemeta_const.inc +793 -0
- package/amxmodx/base/include/fakemeta_stocks.inc +395 -0
- package/amxmodx/base/include/fakemeta_util.inc +882 -0
- package/amxmodx/base/include/file.inc +666 -0
- package/amxmodx/base/include/float.inc +422 -0
- package/amxmodx/base/include/fun.inc +333 -0
- package/amxmodx/base/include/gameconfig.inc +93 -0
- package/amxmodx/base/include/geoip.inc +264 -0
- package/amxmodx/base/include/ham_const.inc +4352 -0
- package/amxmodx/base/include/hamsandwich.inc +439 -0
- package/amxmodx/base/include/hlsdk_const.inc +808 -0
- package/amxmodx/base/include/json.inc +777 -0
- package/amxmodx/base/include/lang.inc +119 -0
- package/amxmodx/base/include/message_const.inc +1130 -0
- package/amxmodx/base/include/message_stocks.inc +161 -0
- package/amxmodx/base/include/messages.inc +604 -0
- package/amxmodx/base/include/newmenus.inc +387 -0
- package/amxmodx/base/include/ns.inc +754 -0
- package/amxmodx/base/include/ns_const.inc +196 -0
- package/amxmodx/base/include/nvault.inc +154 -0
- package/amxmodx/base/include/regex.inc +348 -0
- package/amxmodx/base/include/sockets.inc +183 -0
- package/amxmodx/base/include/sorting.inc +107 -0
- package/amxmodx/base/include/sqlx.inc +582 -0
- package/amxmodx/base/include/string.inc +786 -0
- package/amxmodx/base/include/string_const.inc +156 -0
- package/amxmodx/base/include/string_stocks.inc +325 -0
- package/amxmodx/base/include/textparse_ini.inc +214 -0
- package/amxmodx/base/include/textparse_smc.inc +261 -0
- package/amxmodx/base/include/tfcconst.inc +84 -0
- package/amxmodx/base/include/tfcstats.inc +62 -0
- package/amxmodx/base/include/tfcx.inc +148 -0
- package/amxmodx/base/include/time.inc +118 -0
- package/amxmodx/base/include/tsconst.inc +154 -0
- package/amxmodx/base/include/tsfun.inc +198 -0
- package/amxmodx/base/include/tsstats.inc +65 -0
- package/amxmodx/base/include/tsx.inc +88 -0
- package/amxmodx/base/include/vault.inc +60 -0
- package/amxmodx/base/include/vector.inc +131 -0
- package/amxmodx/base/include/xs.inc +1570 -0
- package/as/amxts.d.ts +153 -0
- package/as/constants.ts +4730 -0
- package/as/effects.ts +1449 -0
- package/as/entities.ts +5482 -0
- package/as/events.ts +3054 -0
- package/as/facade.ts +4639 -0
- package/as/flags.ts +299 -0
- package/as/fs.ts +287 -0
- package/as/hooks.ts +18536 -0
- package/as/kit.ts +25 -0
- package/as/lib/check.ts +86 -0
- package/as/modules/http.ts +184 -0
- package/as/natives.ts +11845 -0
- package/as/os.ts +30 -0
- package/as/promise.ts +1392 -0
- package/as/promise.types.d.ts +82 -0
- package/as/remote.ts +288 -0
- package/as/tsconfig.json +22 -0
- package/as/vector.ts +65 -0
- package/bin/amxts.mjs +10 -0
- package/includes/menu_core.inc +355 -0
- package/includes/order.txt +54 -0
- package/includes/sources.json +27 -0
- package/includes/universal_config.inc +355 -0
- package/lint/oxlint-plugin.mjs +77 -0
- package/node_modules/assemblyscript/LICENSE +201 -0
- package/node_modules/assemblyscript/NOTICE +93 -0
- package/node_modules/assemblyscript/package.json +63 -0
- package/node_modules/assemblyscript/std/README.md +6 -0
- package/node_modules/assemblyscript/std/assembly/array.ts +526 -0
- package/node_modules/assemblyscript/std/assembly/arraybuffer.ts +77 -0
- package/node_modules/assemblyscript/std/assembly/atomics.ts +127 -0
- package/node_modules/assemblyscript/std/assembly/bindings/asyncify.ts +16 -0
- package/node_modules/assemblyscript/std/assembly/bindings/dom.ts +291 -0
- package/node_modules/assemblyscript/std/assembly/bindings/node.ts +6 -0
- package/node_modules/assemblyscript/std/assembly/builtins.ts +2631 -0
- package/node_modules/assemblyscript/std/assembly/compat.ts +2 -0
- package/node_modules/assemblyscript/std/assembly/console.ts +42 -0
- package/node_modules/assemblyscript/std/assembly/crypto.ts +9 -0
- package/node_modules/assemblyscript/std/assembly/dataview.ts +181 -0
- package/node_modules/assemblyscript/std/assembly/date.ts +373 -0
- package/node_modules/assemblyscript/std/assembly/diagnostics.ts +11 -0
- package/node_modules/assemblyscript/std/assembly/error.ts +44 -0
- package/node_modules/assemblyscript/std/assembly/function.ts +38 -0
- package/node_modules/assemblyscript/std/assembly/index.d.ts +2822 -0
- package/node_modules/assemblyscript/std/assembly/iterator.ts +35 -0
- package/node_modules/assemblyscript/std/assembly/map.ts +260 -0
- package/node_modules/assemblyscript/std/assembly/math.ts +3289 -0
- package/node_modules/assemblyscript/std/assembly/memory.ts +123 -0
- package/node_modules/assemblyscript/std/assembly/number.ts +388 -0
- package/node_modules/assemblyscript/std/assembly/object.ts +36 -0
- package/node_modules/assemblyscript/std/assembly/performance.ts +9 -0
- package/node_modules/assemblyscript/std/assembly/polyfills.ts +27 -0
- package/node_modules/assemblyscript/std/assembly/process.ts +50 -0
- package/node_modules/assemblyscript/std/assembly/reference.ts +48 -0
- package/node_modules/assemblyscript/std/assembly/regexp.ts +12 -0
- package/node_modules/assemblyscript/std/assembly/rt/README.md +83 -0
- package/node_modules/assemblyscript/std/assembly/rt/common.ts +81 -0
- package/node_modules/assemblyscript/std/assembly/rt/index-incremental.ts +2 -0
- package/node_modules/assemblyscript/std/assembly/rt/index-minimal.ts +2 -0
- package/node_modules/assemblyscript/std/assembly/rt/index-stub.ts +1 -0
- package/node_modules/assemblyscript/std/assembly/rt/index.d.ts +37 -0
- package/node_modules/assemblyscript/std/assembly/rt/itcms.ts +419 -0
- package/node_modules/assemblyscript/std/assembly/rt/rtrace.ts +15 -0
- package/node_modules/assemblyscript/std/assembly/rt/stub.ts +133 -0
- package/node_modules/assemblyscript/std/assembly/rt/tcms.ts +254 -0
- package/node_modules/assemblyscript/std/assembly/rt/tlsf.ts +592 -0
- package/node_modules/assemblyscript/std/assembly/rt.ts +87 -0
- package/node_modules/assemblyscript/std/assembly/set.ts +220 -0
- package/node_modules/assemblyscript/std/assembly/shared/feature.ts +65 -0
- package/node_modules/assemblyscript/std/assembly/shared/runtime.ts +11 -0
- package/node_modules/assemblyscript/std/assembly/shared/target.ts +11 -0
- package/node_modules/assemblyscript/std/assembly/shared/tsconfig.json +11 -0
- package/node_modules/assemblyscript/std/assembly/shared/typeinfo.ts +72 -0
- package/node_modules/assemblyscript/std/assembly/staticarray.ts +401 -0
- package/node_modules/assemblyscript/std/assembly/string.ts +847 -0
- package/node_modules/assemblyscript/std/assembly/symbol.ts +114 -0
- package/node_modules/assemblyscript/std/assembly/table.ts +16 -0
- package/node_modules/assemblyscript/std/assembly/tsconfig.json +6 -0
- package/node_modules/assemblyscript/std/assembly/typedarray.ts +1945 -0
- package/node_modules/assemblyscript/std/assembly/uri.ts +17 -0
- package/node_modules/assemblyscript/std/assembly/util/bytes.ts +107 -0
- package/node_modules/assemblyscript/std/assembly/util/casemap.ts +497 -0
- package/node_modules/assemblyscript/std/assembly/util/error.ts +54 -0
- package/node_modules/assemblyscript/std/assembly/util/hash.ts +117 -0
- package/node_modules/assemblyscript/std/assembly/util/math.ts +1922 -0
- package/node_modules/assemblyscript/std/assembly/util/memory.ts +290 -0
- package/node_modules/assemblyscript/std/assembly/util/number.ts +873 -0
- package/node_modules/assemblyscript/std/assembly/util/sort.ts +313 -0
- package/node_modules/assemblyscript/std/assembly/util/string.ts +1202 -0
- package/node_modules/assemblyscript/std/assembly/util/uri.ts +275 -0
- package/node_modules/assemblyscript/std/assembly/vector.ts +4 -0
- package/node_modules/assemblyscript/std/assembly.json +16 -0
- package/node_modules/assemblyscript/std/portable/index.d.ts +462 -0
- package/node_modules/assemblyscript/std/portable/index.js +416 -0
- package/node_modules/assemblyscript/std/portable.json +11 -0
- package/node_modules/assemblyscript/std/types/assembly/index.d.ts +1 -0
- package/node_modules/assemblyscript/std/types/assembly/package.json +3 -0
- package/node_modules/assemblyscript/std/types/portable/index.d.ts +1 -0
- package/node_modules/assemblyscript/std/types/portable/package.json +3 -0
- package/node_modules/assemblyscript/tsconfig-base.json +13 -0
- package/package.json +112 -0
- package/runtime/deps/assemblyscript/LICENSE +201 -0
- package/runtime/deps/assemblyscript/NOTICE +93 -0
- package/runtime/deps/assemblyscript/bin/asc.js +35 -0
- package/runtime/deps/assemblyscript/dist/asc.js +25035 -0
- package/runtime/deps/assemblyscript/dist/assemblyscript.js +337 -0
- package/runtime/deps/assemblyscript/node_modules/binaryen/LICENSE +201 -0
- package/runtime/deps/assemblyscript/node_modules/binaryen/index.js +28905 -0
- package/runtime/deps/assemblyscript/node_modules/binaryen/package.json +50 -0
- package/runtime/deps/assemblyscript/node_modules/long/LICENSE +202 -0
- package/runtime/deps/assemblyscript/node_modules/long/index.js +1581 -0
- package/runtime/deps/assemblyscript/node_modules/long/package.json +58 -0
- package/runtime/deps/assemblyscript/package.json +114 -0
- package/runtime/host/amxts.inc +85 -0
- package/runtime/licenses/AMXX-LICENSE.txt +34 -0
- package/runtime/licenses/AssemblyScript-LICENSE +201 -0
- package/runtime/licenses/AssemblyScript-NOTICE +93 -0
- package/runtime/licenses/Binaryen-LICENSE +201 -0
- package/runtime/licenses/Bun-LICENSE.md +84 -0
- package/runtime/licenses/GPL-3.0.txt +674 -0
- package/runtime/licenses/LLVM-LICENSE.TXT +279 -0
- package/runtime/licenses/README.md +25 -0
- package/runtime/licenses/WAMR-LICENSE +219 -0
- package/runtime/natives.txt +1095 -0
- package/runtime/patches/README.md +59 -0
- package/runtime/patches/assemblyscript-0.28.20-amxts.patch +14174 -0
- package/runtime/patches/wamr-2.4.5-amxts.patch +364 -0
- package/scripts/apply-docs.ts +508 -0
- package/scripts/asc.ts +27 -0
- package/scripts/auto-imports.ts +258 -0
- package/scripts/build-wasm.ts +595 -0
- package/scripts/check.ts +93 -0
- package/scripts/compile-cache.ts +179 -0
- package/scripts/compile.ts +401 -0
- package/scripts/docker-server.ts +73 -0
- package/scripts/docs/as/amxts.ts +210 -0
- package/scripts/docs/as/effects.ts +1684 -0
- package/scripts/docs/as/facade.ts +3302 -0
- package/scripts/docs/as/fs.ts +173 -0
- package/scripts/docs/as/lib/check.ts +68 -0
- package/scripts/docs/as/modules/http.ts +66 -0
- package/scripts/docs/as/os.ts +16 -0
- package/scripts/docs/as/promise.ts +132 -0
- package/scripts/docs/as/promise.types.ts +63 -0
- package/scripts/docs/as/vector.ts +48 -0
- package/scripts/docs/entities.ts +2338 -0
- package/scripts/docs/events.ts +245 -0
- package/scripts/docs/game.ts +497 -0
- package/scripts/docs/messages.ts +148 -0
- package/scripts/file-contents.ts +74 -0
- package/scripts/includes.ts +134 -0
- package/scripts/player-fields.ts +866 -0
- package/scripts/plugin-cache.ts +92 -0
- package/scripts/plugin-natives.ts +1157 -0
- package/scripts/prebuilt.ts +268 -0
- package/scripts/prepare.ts +134 -0
- package/scripts/project.ts +973 -0
- package/scripts/run.ts +28 -0
- package/scripts/shared-modules.ts +906 -0
- package/scripts/system.ts +33 -0
- package/scripts/tracked-fs.ts +115 -0
- package/scripts/typed-configs.ts +788 -0
- package/scripts/ui.ts +77 -0
- package/src/cli-api.mjs +175 -0
- package/src/parser/include-parser.ts +506 -0
- package/src/system.mjs +155 -0
- package/src/testing/compile-cache.ts +31 -0
- package/src/testing/compile.ts +222 -0
- package/src/testing/coroutines.ts +191 -0
- package/src/testing/index.ts +39 -0
- package/src/testing/kits.ts +55 -0
- package/src/testing/memory.ts +125 -0
- package/src/testing/natives.ts +925 -0
- package/src/testing/server.ts +1874 -0
- package/src/testing/setup.ts +63 -0
- package/src/testing/tables.ts +139 -0
- package/src/types.ts +51 -0
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
// Typed configs: `configs.load(name, defaults)` of @amxts/config-core.
|
|
2
|
+
//
|
|
3
|
+
// import * as configs from "@amxts/config-core";
|
|
4
|
+
//
|
|
5
|
+
// const settings = configs.load("settings", { chat: { prefix: "[HNS]" }, round: { time: 2.5 } });
|
|
6
|
+
// const other = configs.load<Settings>("other", { ... });
|
|
7
|
+
// settings.round.time = 3;
|
|
8
|
+
// configs.save(settings);
|
|
9
|
+
//
|
|
10
|
+
// The editor types this with a plain generic signature,
|
|
11
|
+
// `load<T>(name: string, defaults: T): T`. AssemblyScript has no reflection:
|
|
12
|
+
// a generic function cannot walk the fields of the object it is given. So the
|
|
13
|
+
// build does it, reading each file on its way to the compiler (Sources.read):
|
|
14
|
+
//
|
|
15
|
+
// - it finds the calls of `load` with defaults and of `save` with one
|
|
16
|
+
// argument, through the file's imports of the module;
|
|
17
|
+
// - it works out the object's shape - from the type argument, the interface
|
|
18
|
+
// or type alias it names (in this file, or imported), or from the defaults'
|
|
19
|
+
// object literal when there is no type - and writes, at the end of the file,
|
|
20
|
+
// a function for that call: it copies the defaults, reads the file into the
|
|
21
|
+
// copy through the module's tree (ConfigNode), with the checks, and
|
|
22
|
+
// remembers how to write the object back (config-core's src/typed.ts);
|
|
23
|
+
// - it puts that function's name in place of `configs.load<...>` - padded, so
|
|
24
|
+
// the rest of the line keeps its columns - and `save` becomes the typed
|
|
25
|
+
// one, which finds the file by the object.
|
|
26
|
+
//
|
|
27
|
+
// The generated code only calls the public ConfigNode, so it is the same in
|
|
28
|
+
// the plugin that owns the module and in any other, where the module is a
|
|
29
|
+
// proxy. The shapes are read from the source text - a syntax tree, no type
|
|
30
|
+
// checker - so a shape is what is written: a literal's values, or types
|
|
31
|
+
// spelled out. What cannot be read that way stops the build with the place
|
|
32
|
+
// and the fix.
|
|
33
|
+
import ts from 'typescript';
|
|
34
|
+
|
|
35
|
+
/** The module, by its place in the tree, as the files' imports have it once rewritten (scripts/project.ts). */
|
|
36
|
+
export const CONFIG_MODULE = '~/modules/config-core';
|
|
37
|
+
|
|
38
|
+
/** What the generated code calls: config-core's src/typed.ts. */
|
|
39
|
+
const HELPERS = `${CONFIG_MODULE}/src/typed`;
|
|
40
|
+
const NS = '__amxtsConfigs';
|
|
41
|
+
|
|
42
|
+
type Scalar = 'text' | 'number' | 'boolean';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What a config value is. `written` is its type as the file writes it, where
|
|
46
|
+
* the file names one (`value as Limits`): the generated interface says the
|
|
47
|
+
* same, so the value is of that type there too.
|
|
48
|
+
*/
|
|
49
|
+
export type Shape = Kinds & { written?: string };
|
|
50
|
+
|
|
51
|
+
type Kinds
|
|
52
|
+
= | { kind: Scalar }
|
|
53
|
+
| { kind: 'name'; names: string[] }
|
|
54
|
+
| { kind: 'list'; of: Shape }
|
|
55
|
+
| { kind: 'object'; fields: Field[] }
|
|
56
|
+
| { kind: 'map'; of: Shape };
|
|
57
|
+
|
|
58
|
+
export interface Field {
|
|
59
|
+
name: string;
|
|
60
|
+
shape: Shape;
|
|
61
|
+
optional: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type ObjectShape = Extract<Shape, { kind: 'object' }>;
|
|
65
|
+
|
|
66
|
+
/** How the transform reaches other files: an import in `from` resolved, with the text the build reads there. */
|
|
67
|
+
export type ImportReader = (from: string, spec: string) => { path: string; text: string } | null;
|
|
68
|
+
|
|
69
|
+
/** A shape that cannot be read, at a node of the file it is in. */
|
|
70
|
+
class ShapeError extends Error {
|
|
71
|
+
constructor(readonly node: ts.Node, readonly file: ts.SourceFile, message: string) {
|
|
72
|
+
super(message);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const WHAT = 'a config value is text, a number, true or false, a union of string literals, a list of them, of lists of them or of objects, an object of them, or a Map<string, ...> of text, numbers or booleans';
|
|
77
|
+
|
|
78
|
+
/** A list's items may be lists of these: rows of values. */
|
|
79
|
+
const ROW_KINDS = ['text', 'number', 'boolean', 'name'];
|
|
80
|
+
|
|
81
|
+
/** Whether a list may hold items of this shape: anything but a Map, and a list only of values. */
|
|
82
|
+
function listable(of: Shape): boolean {
|
|
83
|
+
if (of.kind === 'map') return false;
|
|
84
|
+
return of.kind !== 'list' || ROW_KINDS.includes(of.of.kind);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const NOT_LISTABLE = 'a list holds text, numbers, booleans, names, objects or lists of values - not Maps, or lists of lists or of objects';
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------- reading shapes
|
|
90
|
+
|
|
91
|
+
interface Context {
|
|
92
|
+
path: string;
|
|
93
|
+
file: ts.SourceFile;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class Shapes {
|
|
97
|
+
private files = new Map<string, ts.SourceFile>();
|
|
98
|
+
|
|
99
|
+
constructor(private imports: ImportReader) {}
|
|
100
|
+
|
|
101
|
+
parsed(path: string, text: string): ts.SourceFile {
|
|
102
|
+
const key = `${path}\0${text}`;
|
|
103
|
+
let file = this.files.get(key);
|
|
104
|
+
if (!file) {
|
|
105
|
+
file = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
106
|
+
this.files.set(key, file);
|
|
107
|
+
}
|
|
108
|
+
return file;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A type as written in a file. */
|
|
112
|
+
ofType(node: ts.TypeNode, at: Context, seen: string[] = []): Shape {
|
|
113
|
+
const fail = (message: string) => new ShapeError(node, at.file, message);
|
|
114
|
+
|
|
115
|
+
if (node.kind === ts.SyntaxKind.StringKeyword) return { kind: 'text' };
|
|
116
|
+
if (node.kind === ts.SyntaxKind.NumberKeyword) return { kind: 'number' };
|
|
117
|
+
if (node.kind === ts.SyntaxKind.BooleanKeyword) return { kind: 'boolean' };
|
|
118
|
+
if (ts.isParenthesizedTypeNode(node)) return this.ofType(node.type, at, seen);
|
|
119
|
+
if (ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword) return this.ofType(node.type, at, seen);
|
|
120
|
+
if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) return { kind: 'name', names: [node.literal.text] };
|
|
121
|
+
|
|
122
|
+
if (ts.isUnionTypeNode(node)) {
|
|
123
|
+
const names = node.types.map((each) => {
|
|
124
|
+
const shape = this.ofType(each, at, seen);
|
|
125
|
+
if (shape.kind !== 'name') throw fail(`${node.getText(at.file)} - a union is of string literals only, "a" | "b"; for "not set" write the field with ?`);
|
|
126
|
+
return shape.names;
|
|
127
|
+
});
|
|
128
|
+
return { kind: 'name', names: names.flat() };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (ts.isArrayTypeNode(node)) return this.list(node.elementType, at, seen, fail);
|
|
132
|
+
if (ts.isTypeLiteralNode(node)) return this.members(node.members, at, seen);
|
|
133
|
+
|
|
134
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
135
|
+
const name = node.typeName.getText(at.file);
|
|
136
|
+
const args = node.typeArguments ?? [];
|
|
137
|
+
if ((name === 'Array' || name === 'ReadonlyArray') && args.length === 1) return this.list(args[0], at, seen, fail);
|
|
138
|
+
|
|
139
|
+
if (name === 'Map' && args.length === 2) {
|
|
140
|
+
if (args[0].kind !== ts.SyntaxKind.StringKeyword) throw fail(`${node.getText(at.file)} - a Map's keys are string`);
|
|
141
|
+
const of = this.ofType(args[1], at, seen);
|
|
142
|
+
if (of.kind === 'list' || of.kind === 'object' || of.kind === 'map') throw fail(`${node.getText(at.file)} - a Map holds text, numbers, booleans or names; for objects write a list of them`);
|
|
143
|
+
return { kind: 'map', of };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (args.length) throw fail(`${node.getText(at.file)} - a generic type cannot be a config's; write the type out`);
|
|
147
|
+
return this.named(node.typeName, at, seen);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
throw fail(`${node.getText(at.file)} - ${WHAT}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private list(element: ts.TypeNode, at: Context, seen: string[], fail: (message: string) => Error): Shape {
|
|
154
|
+
const of = this.ofType(element, at, seen);
|
|
155
|
+
if (!listable(of)) throw fail(`${element.getText(at.file)}[] - ${NOT_LISTABLE}`);
|
|
156
|
+
return { kind: 'list', of };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private members(members: ts.NodeArray<ts.TypeElement>, at: Context, seen: string[]): ObjectShape {
|
|
160
|
+
const fields: Field[] = [];
|
|
161
|
+
for (const member of members) {
|
|
162
|
+
if (!ts.isPropertySignature(member) || !member.type) {
|
|
163
|
+
throw new ShapeError(member, at.file, `${member.getText(at.file).trim()} - an object of a config has fields only, each with its type`);
|
|
164
|
+
}
|
|
165
|
+
if (!ts.isIdentifier(member.name)) {
|
|
166
|
+
throw new ShapeError(member, at.file, `${member.name.getText(at.file)} - a field of a config is named with a word, not a string`);
|
|
167
|
+
}
|
|
168
|
+
fields.push({ name: member.name.text, shape: this.ofType(member.type, at, seen), optional: !!member.questionToken });
|
|
169
|
+
}
|
|
170
|
+
return { kind: 'object', fields };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** A type by its name: declared in the file, imported by name, or reached through a namespace import. */
|
|
174
|
+
private named(name: ts.EntityName, at: Context, seen: string[]): Shape {
|
|
175
|
+
const found = this.declaration(name, at);
|
|
176
|
+
if (!found) throw new ShapeError(name, at.file, `${name.getText(at.file)} - not declared in this file or imported into it; a config's type is an interface or a type alias`);
|
|
177
|
+
const key = `${found.at.path}#${found.node.name.text}`;
|
|
178
|
+
if (seen.includes(key)) throw new ShapeError(name, at.file, `${name.getText(at.file)} - a config's type cannot contain itself`);
|
|
179
|
+
const inside = [...seen, key];
|
|
180
|
+
|
|
181
|
+
if (ts.isTypeAliasDeclaration(found.node)) {
|
|
182
|
+
if (found.node.typeParameters?.length) throw new ShapeError(name, at.file, `${name.getText(at.file)} - a generic type cannot be a config's; write the type out`);
|
|
183
|
+
return this.ofType(found.node.type, found.at, inside);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const declaration = found.node;
|
|
187
|
+
if (declaration.typeParameters?.length || declaration.heritageClauses?.length) {
|
|
188
|
+
throw new ShapeError(name, at.file, `${name.getText(at.file)} - an interface that is generic or extends another cannot be a config's; write its fields out`);
|
|
189
|
+
}
|
|
190
|
+
return this.members(declaration.members, found.at, inside);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private declaration(name: ts.EntityName, at: Context): { node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration; at: Context } | null {
|
|
194
|
+
if (ts.isQualifiedName(name)) {
|
|
195
|
+
if (!ts.isIdentifier(name.left)) return null;
|
|
196
|
+
const namespace = this.importOf(name.left.text, at);
|
|
197
|
+
if (!namespace || namespace.kind !== 'namespace') return null;
|
|
198
|
+
return this.exported(name.right.text, namespace.at, []);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const local = this.local(name.text, at);
|
|
202
|
+
if (local) return local;
|
|
203
|
+
const imported = this.importOf(name.text, at);
|
|
204
|
+
if (!imported || imported.kind !== 'named') return null;
|
|
205
|
+
return this.exported(imported.name, imported.at, []);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private local(name: string, at: Context) {
|
|
209
|
+
for (const statement of at.file.statements) {
|
|
210
|
+
if ((ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) && statement.name.text === name) return { node: statement, at };
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** What a local name is imported as: a name of another file, or that whole file. */
|
|
216
|
+
private importOf(local: string, at: Context): { kind: 'named'; name: string; at: Context } | { kind: 'namespace'; at: Context } | null {
|
|
217
|
+
for (const statement of at.file.statements) {
|
|
218
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
219
|
+
const bindings = statement.importClause?.namedBindings;
|
|
220
|
+
if (!bindings) continue;
|
|
221
|
+
const spec = statement.moduleSpecifier.text;
|
|
222
|
+
|
|
223
|
+
if (ts.isNamespaceImport(bindings) && bindings.name.text === local) {
|
|
224
|
+
const file = this.open(at, spec);
|
|
225
|
+
return file ? { kind: 'namespace', at: file } : null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!ts.isNamedImports(bindings)) continue;
|
|
229
|
+
const element = bindings.elements.find(each => each.name.text === local);
|
|
230
|
+
if (!element) continue;
|
|
231
|
+
const file = this.open(at, spec);
|
|
232
|
+
return file ? { kind: 'named', name: (element.propertyName ?? element.name).text, at: file } : null;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** A name a file exports: declared there, or passed on from another file. */
|
|
238
|
+
private exported(name: string, at: Context, visited: string[]): { node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration; at: Context } | null {
|
|
239
|
+
if (visited.includes(at.path)) return null;
|
|
240
|
+
const local = this.local(name, at);
|
|
241
|
+
if (local) return local;
|
|
242
|
+
|
|
243
|
+
for (const statement of at.file.statements) {
|
|
244
|
+
if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
245
|
+
const clause = statement.exportClause;
|
|
246
|
+
let wanted = name;
|
|
247
|
+
|
|
248
|
+
if (clause && ts.isNamedExports(clause)) {
|
|
249
|
+
const element = clause.elements.find(each => each.name.text === name);
|
|
250
|
+
if (!element) continue;
|
|
251
|
+
wanted = (element.propertyName ?? element.name).text;
|
|
252
|
+
} else if (clause) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const file = this.open(at, statement.moduleSpecifier.text);
|
|
257
|
+
const found = file ? this.exported(wanted, file, [...visited, at.path]) : null;
|
|
258
|
+
if (found) return found;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const alias = this.importOf(name, at);
|
|
262
|
+
return alias && alias.kind === 'named' ? this.exported(alias.name, alias.at, [...visited, at.path]) : null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private open(at: Context, spec: string): Context | null {
|
|
266
|
+
const found = this.imports(at.path, spec);
|
|
267
|
+
return found ? { path: found.path, file: this.parsed(found.path, found.text) } : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The defaults' value, when no type is given: what it visibly is. */
|
|
271
|
+
ofValue(node: ts.Expression, at: Context): Shape {
|
|
272
|
+
const fail = (message: string) => new ShapeError(node, at.file, message);
|
|
273
|
+
|
|
274
|
+
if (ts.isParenthesizedExpression(node)) return this.ofValue(node.expression, at);
|
|
275
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) return { ...this.ofType(node.type, at), written: node.type.getText(at.file) };
|
|
276
|
+
if (ts.isSatisfiesExpression(node)) return this.ofValue(node.expression, at);
|
|
277
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateExpression(node)) return { kind: 'text' };
|
|
278
|
+
if (ts.isNumericLiteral(node)) return { kind: 'number' };
|
|
279
|
+
if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) return { kind: 'number' };
|
|
280
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return { kind: 'boolean' };
|
|
281
|
+
|
|
282
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
283
|
+
const fields: Field[] = [];
|
|
284
|
+
for (const property of node.properties) {
|
|
285
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
|
|
286
|
+
throw new ShapeError(property, at.file, `${property.getText(at.file)} - a field of the defaults is \`name: value\`; for anything else give the type: configs.load<Settings>(...)`);
|
|
287
|
+
}
|
|
288
|
+
fields.push({ name: property.name.text, shape: this.ofValue(property.initializer, at), optional: false });
|
|
289
|
+
}
|
|
290
|
+
return { kind: 'object', fields };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
294
|
+
if (node.elements.length === 0) throw fail(`[] - an empty list says nothing of its items; give the type: configs.load<Settings>(...), or write the list with an item`);
|
|
295
|
+
const items = node.elements.map(each => this.ofValue(each, at));
|
|
296
|
+
const first = items[0];
|
|
297
|
+
if (!listable(first)) throw fail(`${node.getText(at.file)} - ${NOT_LISTABLE}`);
|
|
298
|
+
if (items.some(each => describe(each) !== describe(first))) throw fail(`${node.getText(at.file)} - the items are not of one kind; give the type: configs.load<Settings>(...)`);
|
|
299
|
+
return { kind: 'list', of: first };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'Map' && node.typeArguments?.length === 2) {
|
|
303
|
+
if (node.typeArguments[0].kind !== ts.SyntaxKind.StringKeyword) throw fail(`${node.getText(at.file)} - a Map's keys are string`);
|
|
304
|
+
const of = this.ofType(node.typeArguments[1], at);
|
|
305
|
+
if (of.kind === 'list' || of.kind === 'object' || of.kind === 'map') throw fail(`${node.getText(at.file)} - a Map holds text, numbers, booleans or names; for objects write a list of them`);
|
|
306
|
+
return { kind: 'map', of };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
throw fail(`${node.getText(at.file)} - its type is not seen from the value; give the type: configs.load<Settings>(...)`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** A shape as one line: equal shapes, equal lines. */
|
|
314
|
+
export function describe(shape: Shape): string {
|
|
315
|
+
if (shape.written) return shape.written;
|
|
316
|
+
switch (shape.kind) {
|
|
317
|
+
case 'name': return shape.names.map(each => JSON.stringify(each)).join(' | ');
|
|
318
|
+
case 'list': return shape.of.kind === 'name' ? `(${describe(shape.of)})[]` : `${describe(shape.of)}[]`;
|
|
319
|
+
case 'map': return `Map<string, ${describe(shape.of)}>`;
|
|
320
|
+
case 'object': return `{ ${shape.fields.map(f => `${f.name}${f.optional ? '?' : ''}: ${describe(f.shape)}`).join('; ')} }`;
|
|
321
|
+
case 'text': return 'string';
|
|
322
|
+
default: return shape.kind;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ---------------------------------------------------------------- the code
|
|
327
|
+
|
|
328
|
+
/** One load's function: copies the defaults, reads the file into the copy, remembers how to write it back. */
|
|
329
|
+
class LoadWriter {
|
|
330
|
+
private lines: string[] = [];
|
|
331
|
+
private count = 0;
|
|
332
|
+
|
|
333
|
+
constructor(private readonly shape: ObjectShape) {}
|
|
334
|
+
|
|
335
|
+
private temp(prefix: string) {
|
|
336
|
+
return `${prefix}${this.count++}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private emit(depth: number, line: string) {
|
|
340
|
+
this.lines.push(`${'\t'.repeat(depth)}${line}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function(name: string, type: string): string {
|
|
344
|
+
const shape = this.shape;
|
|
345
|
+
const ini = iniMisfits(shape);
|
|
346
|
+
this.emit(1, `const root = ${NS}.open(name, ${JSON.stringify(ini.outside)}, ${JSON.stringify(ini.objectLists)});`);
|
|
347
|
+
this.emit(1, `const value: ${type} = ${literalOf(shape, 'defaults')};`);
|
|
348
|
+
this.copyRest(shape, 'defaults', 'value', 1);
|
|
349
|
+
this.emit(1, `const top = ${NS}.objectOf(root, ${JSON.stringify(shape.fields.map(f => f.name))});`);
|
|
350
|
+
this.emit(1, 'if (top != null) {');
|
|
351
|
+
this.read(shape, 'top', 'value', 2);
|
|
352
|
+
this.emit(1, '}');
|
|
353
|
+
this.emit(1, `${NS}.track(value, root, () => {`);
|
|
354
|
+
this.write(shape, 'value', '', 2);
|
|
355
|
+
this.emit(1, '});');
|
|
356
|
+
this.emit(1, 'return value;');
|
|
357
|
+
return [`function ${name}(name: string, defaults: ${type}) {`, ...this.lines, '}'].join('\n');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** What a copy's literal leaves out: optional fields, lists of objects, Maps. */
|
|
361
|
+
private copyRest(shape: ObjectShape, source: string, target: string, depth: number) {
|
|
362
|
+
for (const field of shape.fields) {
|
|
363
|
+
const from = `${source}.${field.name}`;
|
|
364
|
+
const to = `${target}.${field.name}`;
|
|
365
|
+
|
|
366
|
+
if (field.optional) {
|
|
367
|
+
this.emit(depth, `if (${from} !== undefined) {`);
|
|
368
|
+
this.copyValue(field.shape, present(field.shape, from), to, depth + 1, true);
|
|
369
|
+
this.emit(depth, '}');
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
this.copyValue(field.shape, from, to, depth, false);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Copies what the literal did not: all of it, for an optional field. */
|
|
378
|
+
private copyValue(shape: Shape, from: string, to: string, depth: number, whole: boolean) {
|
|
379
|
+
const target = whole ? present(shape, to) : to;
|
|
380
|
+
if (whole && (shape.kind !== 'list' || shape.of.kind !== 'object') && shape.kind !== 'map') this.emit(depth, `${to} = ${copyOf(shape, from)};`);
|
|
381
|
+
if (shape.kind === 'object') this.copyRest(shape, from, target, depth);
|
|
382
|
+
|
|
383
|
+
if (shape.kind === 'list' && shape.of.kind === 'object') {
|
|
384
|
+
const item = this.temp('item');
|
|
385
|
+
const made = this.temp('made');
|
|
386
|
+
if (whole) this.emit(depth, `${to} = [];`);
|
|
387
|
+
this.emit(depth, `for (const ${item} of ${from}) {`);
|
|
388
|
+
this.emit(depth + 1, `${target}.push(${literalOf(shape.of, item)});`);
|
|
389
|
+
this.emit(depth + 1, `const ${made} = ${target}[${target}.length - 1];`);
|
|
390
|
+
this.copyRest(shape.of, item, made, depth + 1);
|
|
391
|
+
this.emit(depth, '}');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (shape.kind === 'map') {
|
|
395
|
+
const key = this.temp('key');
|
|
396
|
+
if (whole) this.emit(depth, `${to} = new Map<string, ${typeOf(shape.of)}>();`);
|
|
397
|
+
this.emit(depth, `for (const ${key} of ${from}.keys()) ${target}.set(${key}, ${from}.get(${key}));`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Reads an object's fields from `node` - an object, checked - into `target`. */
|
|
402
|
+
private read(shape: ObjectShape, node: string, target: string, depth: number) {
|
|
403
|
+
for (const field of shape.fields) {
|
|
404
|
+
const value = this.temp('node');
|
|
405
|
+
const to = `${target}.${field.name}`;
|
|
406
|
+
this.emit(depth, `const ${value} = ${NS}.member(${node}, ${JSON.stringify(field.name)});`);
|
|
407
|
+
this.readValue(field.shape, value, to, field.optional, depth);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private readValue(shape: Shape, value: string, to: string, optional: boolean, depth: number) {
|
|
412
|
+
switch (shape.kind) {
|
|
413
|
+
case 'text':
|
|
414
|
+
case 'number':
|
|
415
|
+
case 'boolean':
|
|
416
|
+
this.emit(depth, `if (${NS}.is(${value}, "${shape.kind}")) ${to} = ${NS}.${converter(shape.kind)}(${value});`);
|
|
417
|
+
return;
|
|
418
|
+
case 'name':
|
|
419
|
+
this.emit(depth, `if (${NS}.is(${value}, "name", ${JSON.stringify(shape.names)})) ${to} = ${NS}.asText(${value});`);
|
|
420
|
+
return;
|
|
421
|
+
case 'list':
|
|
422
|
+
this.readList(shape.of, value, to, optional, depth);
|
|
423
|
+
return;
|
|
424
|
+
case 'map':
|
|
425
|
+
this.readMap(shape, value, to, optional, depth);
|
|
426
|
+
return;
|
|
427
|
+
case 'object': {
|
|
428
|
+
const node = this.temp('object');
|
|
429
|
+
this.emit(depth, `const ${node} = ${NS}.objectOf(${value}, ${JSON.stringify(shape.fields.map(f => f.name))});`);
|
|
430
|
+
this.emit(depth, `if (${node} != null) {`);
|
|
431
|
+
|
|
432
|
+
if (optional) {
|
|
433
|
+
this.emit(depth + 1, `if (${to} === undefined) {`);
|
|
434
|
+
this.emit(depth + 2, `${to} = ${zeroOf(shape)};`);
|
|
435
|
+
this.emit(depth + 2, `${NS}.need(${node}, ${JSON.stringify(required(shape))});`);
|
|
436
|
+
this.emit(depth + 1, '}');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
this.read(shape, node, optional ? present(shape, to) : to, depth + 1);
|
|
440
|
+
this.emit(depth, '}');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private readList(of: Shape, value: string, to: string, optional: boolean, depth: number) {
|
|
446
|
+
if (of.kind === 'list') {
|
|
447
|
+
const rows = this.temp('rows');
|
|
448
|
+
this.emit(depth, `const ${rows} = ${NS}.lists(${value});`);
|
|
449
|
+
this.emit(depth, `if (${rows} != null) ${to} = ${rows}.map<${typeOf(of)}>(row => ${rowReader(of.of)});`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (of.kind === 'name') {
|
|
454
|
+
this.emit(depth, `if (${NS}.is(${value}, "names")) ${to} = ${NS}.asNames(${value}, ${JSON.stringify(of.names)});`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (of.kind !== 'object') {
|
|
459
|
+
this.emit(depth, `if (${NS}.is(${value}, "${of.kind}s")) ${to} = ${NS}.${converter(of.kind)}s(${value});`);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const items = this.temp('items');
|
|
464
|
+
const item = this.temp('item');
|
|
465
|
+
const made = this.temp('made');
|
|
466
|
+
const list = optional ? `${to}!` : to;
|
|
467
|
+
this.emit(depth, `const ${items} = ${NS}.items(${value});`);
|
|
468
|
+
this.emit(depth, `if (${items} != null) {`);
|
|
469
|
+
this.emit(depth + 1, `${to} = [];`);
|
|
470
|
+
this.emit(depth + 1, `for (const ${item} of ${items}) {`);
|
|
471
|
+
this.emit(depth + 2, `${NS}.objectOf(${item}, ${JSON.stringify(of.fields.map(f => f.name))});`);
|
|
472
|
+
this.emit(depth + 2, `${NS}.need(${item}, ${JSON.stringify(required(of))});`);
|
|
473
|
+
this.emit(depth + 2, `${list}.push(${zeroOf(of)});`);
|
|
474
|
+
this.emit(depth + 2, `const ${made} = ${list}[${list}.length - 1];`);
|
|
475
|
+
this.read(of, item, made, depth + 2);
|
|
476
|
+
this.emit(depth + 1, '}');
|
|
477
|
+
this.emit(depth, '}');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private readMap(shape: Extract<Shape, { kind: 'map' }>, value: string, to: string, optional: boolean, depth: number) {
|
|
481
|
+
const entries = this.temp('entries');
|
|
482
|
+
const entry = this.temp('entry');
|
|
483
|
+
const map = optional ? `${to}!` : to;
|
|
484
|
+
const kind = shape.of.kind === 'name' ? 'name' : shape.of.kind;
|
|
485
|
+
const read = converter(shape.of.kind === 'name' ? 'text' : shape.of.kind);
|
|
486
|
+
const names = shape.of.kind === 'name' ? `, ${JSON.stringify(shape.of.names)}` : '';
|
|
487
|
+
this.emit(depth, `const ${entries} = ${NS}.entries(${value});`);
|
|
488
|
+
this.emit(depth, `if (${entries} != null) {`);
|
|
489
|
+
this.emit(depth + 1, `${to} = new Map<string, ${typeOf(shape.of)}>();`);
|
|
490
|
+
this.emit(depth + 1, `for (const ${entry} of ${entries}) {`);
|
|
491
|
+
this.emit(depth + 2, `if (${NS}.is(${entry}, "${kind}"${names})) ${map}.set(${entry}.key, ${NS}.${read}(${entry}));`);
|
|
492
|
+
this.emit(depth + 1, '}');
|
|
493
|
+
this.emit(depth, '}');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Writes an object's fields into the file's tree, at `path` - a template literal's text. */
|
|
497
|
+
private write(shape: ObjectShape, source: string, path: string, depth: number) {
|
|
498
|
+
for (const field of shape.fields) {
|
|
499
|
+
const from = `${source}.${field.name}`;
|
|
500
|
+
const at = path ? `${path}.${field.name}` : field.name;
|
|
501
|
+
|
|
502
|
+
if (field.optional) {
|
|
503
|
+
this.emit(depth, `if (${from} !== undefined) {`);
|
|
504
|
+
this.writeValue(field.shape, present(field.shape, from), at, depth + 1);
|
|
505
|
+
this.emit(depth, '} else {');
|
|
506
|
+
this.emit(depth + 1, `root.remove(\`${at}\`);`);
|
|
507
|
+
this.emit(depth, '}');
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
this.writeValue(field.shape, from, at, depth);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private writeValue(shape: Shape, from: string, at: string, depth: number) {
|
|
516
|
+
const path = `\`${at}\``;
|
|
517
|
+
switch (shape.kind) {
|
|
518
|
+
case 'text':
|
|
519
|
+
case 'name':
|
|
520
|
+
this.emit(depth, `${NS}.putText(root, ${path}, ${from});`);
|
|
521
|
+
return;
|
|
522
|
+
case 'number':
|
|
523
|
+
case 'boolean':
|
|
524
|
+
this.emit(depth, `${NS}.${writer(shape.kind)}(root, ${path}, ${from});`);
|
|
525
|
+
return;
|
|
526
|
+
case 'object':
|
|
527
|
+
this.write(shape, from, at, depth);
|
|
528
|
+
return;
|
|
529
|
+
case 'map': {
|
|
530
|
+
const key = this.temp('key');
|
|
531
|
+
this.emit(depth, `${NS}.keep(root, ${path}, ${from}.keys());`);
|
|
532
|
+
this.emit(depth, `for (const ${key} of ${from}.keys()) ${NS}.${writer(shape.of.kind)}(root, \`${at}.\${${key}}\`, ${from}.get(${key}));`);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
case 'list':
|
|
536
|
+
this.writeList(shape.of, from, at, depth);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private writeList(of: Shape, from: string, at: string, depth: number) {
|
|
541
|
+
const path = `\`${at}\``;
|
|
542
|
+
if (of.kind === 'text' || of.kind === 'name' || of.kind === 'number' || of.kind === 'boolean') {
|
|
543
|
+
this.emit(depth, `${NS}.${writer(of.kind)}s(root, ${path}, ${from});`);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (of.kind === 'list') {
|
|
547
|
+
const index = this.temp('index');
|
|
548
|
+
const row = this.temp('row');
|
|
549
|
+
this.emit(depth, `${NS}.resizeLists(root, ${path}, ${from}.length);`);
|
|
550
|
+
this.emit(depth, `let ${index} = 0;`);
|
|
551
|
+
this.emit(depth, `for (const ${row} of ${from}) {`);
|
|
552
|
+
this.emit(depth + 1, `${NS}.${writer(of.of.kind)}s(root, \`${at}[\${${index}}]\`, ${row});`);
|
|
553
|
+
this.emit(depth + 1, `${index}++;`);
|
|
554
|
+
this.emit(depth, '}');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (of.kind !== 'object') return;
|
|
559
|
+
|
|
560
|
+
// An INI file has no list of objects: it keeps what it has.
|
|
561
|
+
const index = this.temp('index');
|
|
562
|
+
const item = this.temp('item');
|
|
563
|
+
this.emit(depth, 'if (root.format != "ini") {');
|
|
564
|
+
this.emit(depth + 1, `${NS}.resize(root, ${path}, ${from}.length);`);
|
|
565
|
+
this.emit(depth + 1, `let ${index} = 0;`);
|
|
566
|
+
this.emit(depth + 1, `for (const ${item} of ${from}) {`);
|
|
567
|
+
this.write(of, item, `${at}[\${${index}}]`, depth + 2);
|
|
568
|
+
this.emit(depth + 2, `${index}++;`);
|
|
569
|
+
this.emit(depth + 1, '}');
|
|
570
|
+
this.emit(depth, '}');
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** The type as AssemblyScript reads it; an object type in place is a class of its own there (Parser.parseTypeLiteral). */
|
|
575
|
+
export function typeOf(shape: Shape): string {
|
|
576
|
+
return describe(shape);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** The helper that reads a value as this kind: asText, asNumber, asBoolean. */
|
|
580
|
+
function converter(kind: string): string {
|
|
581
|
+
return `as${kind[0].toUpperCase()}${kind.slice(1)}`;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** A row of a list of lists read as its values: asTexts(row), asNames(row, [...]). */
|
|
585
|
+
function rowReader(of: Shape): string {
|
|
586
|
+
if (of.kind === 'name') return `${NS}.asNames(row, ${JSON.stringify(of.names)})`;
|
|
587
|
+
return `${NS}.${converter(of.kind)}s(row)`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** The helper that writes a value of this kind where it changed: putText, putNumber, putBoolean. */
|
|
591
|
+
function writer(kind: string): string {
|
|
592
|
+
return kind === 'name' ? 'putText' : `put${kind[0].toUpperCase()}${kind.slice(1)}`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** An optional field known to be there: an object's, a string's or a list's `!`; a number and a boolean are values. */
|
|
596
|
+
function present(shape: Shape, from: string): string {
|
|
597
|
+
return shape.kind === 'number' || shape.kind === 'boolean' ? from : `${from}!`;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function required(shape: ObjectShape): string[] {
|
|
601
|
+
return shape.fields.filter(f => !f.optional).map(f => f.name);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** A copy of a value, as an expression: a list of objects and a Map are filled after it. */
|
|
605
|
+
function copyOf(shape: Shape, from: string): string {
|
|
606
|
+
switch (shape.kind) {
|
|
607
|
+
case 'object': return literalOf(shape, from);
|
|
608
|
+
case 'list':
|
|
609
|
+
if (shape.of.kind === 'object') return '[]';
|
|
610
|
+
return shape.of.kind === 'list' ? `${from}.map<${typeOf(shape.of)}>(row => row.slice(0))` : `${from}.slice(0)`;
|
|
611
|
+
case 'map': return `new Map<string, ${typeOf(shape.of)}>()`;
|
|
612
|
+
default: return from;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** An object literal copying the required fields of `from`. */
|
|
617
|
+
function literalOf(shape: ObjectShape, from: string): string {
|
|
618
|
+
const fields = shape.fields.filter(f => !f.optional).map(f => `${f.name}: ${copyOf(f.shape, `${from}.${f.name}`)}`);
|
|
619
|
+
return fields.length ? `{ ${fields.join(', ')} }` : '{}';
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** An object made from the file: each required field empty, the optional ones left out. */
|
|
623
|
+
function zeroOf(shape: Shape): string {
|
|
624
|
+
switch (shape.kind) {
|
|
625
|
+
case 'text': return '""';
|
|
626
|
+
case 'number': return '0';
|
|
627
|
+
case 'boolean': return 'false';
|
|
628
|
+
case 'name': return JSON.stringify(shape.names[0]);
|
|
629
|
+
case 'list': return '[]';
|
|
630
|
+
case 'map': return `new Map<string, ${typeOf(shape.of)}>()`;
|
|
631
|
+
case 'object': {
|
|
632
|
+
const fields = shape.fields.filter(f => !f.optional).map(f => `${f.name}: ${zeroOf(f.shape)}`);
|
|
633
|
+
return fields.length ? `{ ${fields.join(', ')} }` : '{}';
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* What an INI file has no place for: the values at the top that are not
|
|
640
|
+
* objects - an INI file holds [sections] of values - and lists of objects.
|
|
641
|
+
*/
|
|
642
|
+
export function iniMisfits(shape: ObjectShape): { outside: string[]; objectLists: string[] } {
|
|
643
|
+
const outside: string[] = [];
|
|
644
|
+
const objectLists: string[] = [];
|
|
645
|
+
const walk = (object: ObjectShape, path: string) => {
|
|
646
|
+
for (const field of object.fields) {
|
|
647
|
+
const at = path ? `${path}.${field.name}` : field.name;
|
|
648
|
+
if (field.shape.kind === 'list' && field.shape.of.kind === 'object') objectLists.push(at);
|
|
649
|
+
else if (!path && field.shape.kind !== 'object' && field.shape.kind !== 'map') outside.push(at);
|
|
650
|
+
else if (field.shape.kind === 'object') walk(field.shape, at);
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
walk(shape, '');
|
|
654
|
+
return { outside, objectLists };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ---------------------------------------------------------------- one file
|
|
658
|
+
|
|
659
|
+
export interface TypedConfigs {
|
|
660
|
+
/** The file as the compiler reads it: the calls replaced, the functions after its last line. */
|
|
661
|
+
text: string;
|
|
662
|
+
/** What stopped a call: `file:line:column: ...`. */
|
|
663
|
+
problems: string[];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* The typed calls of one file made into generated code. `path` is the file's
|
|
668
|
+
* place in the tree, `display` how a message names it, `imports` how a type
|
|
669
|
+
* imported from another file is found.
|
|
670
|
+
*/
|
|
671
|
+
export function typedConfigs(path: string, display: string, text: string, imports: ImportReader): TypedConfigs {
|
|
672
|
+
if (!text.includes(CONFIG_MODULE)) return { text, problems: [] };
|
|
673
|
+
|
|
674
|
+
const shapes = new Shapes(imports);
|
|
675
|
+
const file = shapes.parsed(path, text);
|
|
676
|
+
const namespaces = new Set<string>();
|
|
677
|
+
const loads = new Set<string>();
|
|
678
|
+
const saves = new Set<string>();
|
|
679
|
+
|
|
680
|
+
for (const statement of file.statements) {
|
|
681
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== CONFIG_MODULE) continue;
|
|
682
|
+
const bindings = statement.importClause?.namedBindings;
|
|
683
|
+
if (bindings && ts.isNamespaceImport(bindings)) namespaces.add(bindings.name.text);
|
|
684
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
685
|
+
for (const element of bindings.elements) {
|
|
686
|
+
const imported = (element.propertyName ?? element.name).text;
|
|
687
|
+
if (imported === 'load') loads.add(element.name.text);
|
|
688
|
+
if (imported === 'save') saves.add(element.name.text);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (!namespaces.size && !loads.size && !saves.size) return { text, problems: [] };
|
|
692
|
+
|
|
693
|
+
const isCall = (callee: ts.Expression, name: string, locals: Set<string>) =>
|
|
694
|
+
(ts.isIdentifier(callee) && locals.has(callee.text))
|
|
695
|
+
|| (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) && namespaces.has(callee.expression.text) && callee.name.text === name);
|
|
696
|
+
|
|
697
|
+
const edits: { start: number; end: number; with: string }[] = [];
|
|
698
|
+
const functions: string[] = [];
|
|
699
|
+
const problems: string[] = [];
|
|
700
|
+
const at: Context = { path, file };
|
|
701
|
+
|
|
702
|
+
const replace = (call: ts.CallExpression, name: string) => {
|
|
703
|
+
const start = call.expression.getStart(file);
|
|
704
|
+
const end = call.arguments.pos - 1;
|
|
705
|
+
edits.push({ start, end, with: name.padEnd(end - start) });
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const visit = (node: ts.Node) => {
|
|
709
|
+
ts.forEachChild(node, visit);
|
|
710
|
+
if (!ts.isCallExpression(node)) return;
|
|
711
|
+
|
|
712
|
+
if (isCall(node.expression, 'save', saves) && node.arguments.length === 1 && !node.typeArguments) {
|
|
713
|
+
replace(node, `${NS}.save`);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const typed = node.typeArguments?.length || node.arguments.length >= 2;
|
|
718
|
+
if (!isCall(node.expression, 'load', loads) || !typed) return;
|
|
719
|
+
|
|
720
|
+
try {
|
|
721
|
+
const { shape, type, declaration } = shapeOfCall(node, shapes, at, functions.length);
|
|
722
|
+
if (shape.kind !== 'object') throw new ShapeError(node, file, `a config is an object: configs.load(name, { ... })`);
|
|
723
|
+
const name = `__amxtsConfigLoad${functions.length}`;
|
|
724
|
+
if (declaration) functions.push(declaration);
|
|
725
|
+
functions.push(new LoadWriter(shape).function(name, type));
|
|
726
|
+
replace(node, name);
|
|
727
|
+
} catch (problem) {
|
|
728
|
+
if (!(problem instanceof ShapeError)) throw problem;
|
|
729
|
+
const place = problem.file.getLineAndCharacterOfPosition(problem.node.getStart(problem.file));
|
|
730
|
+
const where = problem.file === file ? display : problem.file.fileName;
|
|
731
|
+
problems.push(`${where}:${place.line + 1}:${place.character + 1}: configs.load - ${problem.message}`);
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
visit(file);
|
|
735
|
+
|
|
736
|
+
if (!edits.length) return { text, problems };
|
|
737
|
+
|
|
738
|
+
let out = text;
|
|
739
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) out = out.slice(0, edit.start) + edit.with + out.slice(edit.end);
|
|
740
|
+
|
|
741
|
+
// After the last line, so every line above keeps its number.
|
|
742
|
+
const generated = [
|
|
743
|
+
'',
|
|
744
|
+
`// GENERATED by scripts/typed-configs.ts: the typed configs this file loads.`,
|
|
745
|
+
`import * as ${NS} from "${HELPERS}";`,
|
|
746
|
+
...functions,
|
|
747
|
+
'',
|
|
748
|
+
].join('\n');
|
|
749
|
+
return { text: `${out}\n${generated}`, problems };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** A load's shape and the type its function takes: the type argument, the defaults' declared type, or one made from the literal. */
|
|
753
|
+
function shapeOfCall(call: ts.CallExpression, shapes: Shapes, at: Context, index: number): { shape: Shape; type: string; declaration: string | null } {
|
|
754
|
+
const typeArgument = call.typeArguments?.[0];
|
|
755
|
+
if (typeArgument) return { shape: shapes.ofType(typeArgument, at), type: typeArgument.getText(at.file), declaration: null };
|
|
756
|
+
|
|
757
|
+
const defaults = call.arguments[1];
|
|
758
|
+
if (!defaults) throw new ShapeError(call, at.file, 'the defaults are missing: configs.load(name, { ... })');
|
|
759
|
+
|
|
760
|
+
// A variable or a parameter with a declared type: `const defaults: Settings = { ... }`.
|
|
761
|
+
if (ts.isIdentifier(defaults)) {
|
|
762
|
+
const declared = declaredType(defaults, at.file);
|
|
763
|
+
if (!declared) throw new ShapeError(defaults, at.file, `${defaults.text} - its type is not seen here; give it a type, or the call one: configs.load<Settings>(...)`);
|
|
764
|
+
return { shape: shapes.ofType(declared, at), type: declared.getText(at.file), declaration: null };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const shape = shapes.ofValue(defaults, at);
|
|
768
|
+
if (shape.kind !== 'object') return { shape, type: '', declaration: null };
|
|
769
|
+
const name = `__AmxtsConfig${index}`;
|
|
770
|
+
return { shape, type: name, declaration: `interface ${name} ${typeOf(shape)}` };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** The declared type of the variable or parameter a name means where it is used: the nearest one around it. */
|
|
774
|
+
function declaredType(use: ts.Identifier, file: ts.SourceFile): ts.TypeNode | null {
|
|
775
|
+
for (let scope: ts.Node | undefined = use.parent; scope; scope = scope.parent) {
|
|
776
|
+
if (ts.isFunctionLike(scope)) {
|
|
777
|
+
const parameter = scope.parameters.find(each => ts.isIdentifier(each.name) && each.name.text === use.text);
|
|
778
|
+
if (parameter) return parameter.type ?? null;
|
|
779
|
+
}
|
|
780
|
+
if (!ts.isBlock(scope) && !ts.isSourceFile(scope)) continue;
|
|
781
|
+
for (const statement of scope.statements) {
|
|
782
|
+
if (!ts.isVariableStatement(statement) || statement.getStart(file) > use.getStart(file)) continue;
|
|
783
|
+
const found = statement.declarationList.declarations.find(each => ts.isIdentifier(each.name) && each.name.text === use.text);
|
|
784
|
+
if (found) return found.type ?? null;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|