@amxts/config-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.
Binary file
@@ -0,0 +1,197 @@
1
+ // The tooltips of src/index.ts, in both languages: scripts/apply-docs.ts writes the
2
+ // one AMXTS_DOCS_LANG picks into the JSDoc above each element.
3
+ export default {
4
+ "setBaseDir": {
5
+ en: `Sets the folder under \`configs/\` that file names are relative to, e.g. \`"myserver"\`; \`""\` is \`configs/\` itself.`,
6
+ ru: `Задаёт папку внутри \`configs/\`, от которой считаются имена файлов, например \`"myserver"\`; \`""\` — сама \`configs/\`.`,
7
+ },
8
+ "load": {
9
+ en: `
10
+ Reads a config file into an object shaped like \`defaults\`: each value the
11
+ file has, where it is of the right kind, and the default for the rest.
12
+ The file is \`configs/<baseDir>/<name>\` - YAML, JSON or INI, whichever is
13
+ there (\`resolve()\`). A value of the wrong kind, a name not in its union and
14
+ a key the object does not have are said in the server console with the
15
+ file, the line and the column. \`save()\` writes the object back.
16
+
17
+ const settings = configs.load("settings", {
18
+ chat: { prefix: "[HNS]" },
19
+ round: { time: 2.5 },
20
+ });
21
+ settings.round.time = 3;
22
+ configs.save(settings);
23
+ `,
24
+ ru: `
25
+ Читает файл конфига в объект той же формы, что \`defaults\`: каждое значение,
26
+ которое есть в файле и нужного вида, а для остальных — значение по умолчанию.
27
+ Файл — \`configs/<baseDir>/<name>\`: YAML, JSON или INI, какой есть
28
+ (\`resolve()\`). Значение не того вида, имя не из своего юниона и ключ,
29
+ которого у объекта нет, называются в консоли сервера с файлом, строкой и
30
+ столбцом. \`save()\` записывает объект обратно.
31
+
32
+ const settings = configs.load("settings", {
33
+ chat: { prefix: "[HNS]" },
34
+ round: { time: 2.5 },
35
+ });
36
+ settings.round.time = 3;
37
+ configs.save(settings);
38
+ `,
39
+ },
40
+ "save": {
41
+ en: `
42
+ Writes an object \`load()\` read back into its file, in the file's format:
43
+ comments on lines of their own stay where they were. \`false\` when it could
44
+ not be written, or the object was not read by \`load()\`.
45
+ `,
46
+ ru: `
47
+ Записывает объект, прочитанный \`load()\`, обратно в его файл, в формате
48
+ файла: комментарии на отдельных строках остаются на месте. \`false\`, если
49
+ записать не вышло или объект прочитан не через \`load()\`.
50
+ `,
51
+ },
52
+ "ConfigNode": {
53
+ en: `
54
+ A value of a config file: an object, an array, text, a number, a boolean
55
+ or \`null\`, with the place it was read from. \`read()\` gives the file's top
56
+ value; a path leads into it: \`"chat.prefix"\`, \`"items[0].name"\`. For a file
57
+ whose shape is not known beforehand; a config of a known shape is read
58
+ into an object by \`load(name, defaults)\`.
59
+
60
+ const maps = configs.read("maps");
61
+ for (const map of maps.values()) if (map.getBoolean("enabled")) console.log(map.key);
62
+ `,
63
+ ru: `
64
+ Значение файла конфига: объект, массив, текст, число, логическое значение
65
+ или \`null\`, с местом, откуда оно прочитано. \`read()\` отдаёт верхнее значение
66
+ файла; путь ведёт внутрь: \`"chat.prefix"\`, \`"items[0].name"\`. Для файла, форма
67
+ которого заранее неизвестна; конфиг известной формы читается в объект
68
+ через \`load(name, defaults)\`.
69
+
70
+ const maps = configs.read("maps");
71
+ for (const map of maps.values()) if (map.getBoolean("enabled")) console.log(map.key);
72
+ `,
73
+ },
74
+ "ConfigNode.kind": {
75
+ en: `The value's kind, one of \`"object"\`, \`"array"\`, \`"string"\`, \`"number"\`, \`"boolean"\` or \`"null"\`.`,
76
+ ru: `Вид значения, одно из \`"object"\`, \`"array"\`, \`"string"\`, \`"number"\`, \`"boolean"\` или \`"null"\`.`,
77
+ },
78
+ "ConfigNode.key": {
79
+ en: `The value's key in its object, e.g. \`"prefix"\`; \`""\` for an item of an array and for the top value.`,
80
+ ru: `Ключ значения в его объекте, например \`"prefix"\`; \`""\` у элемента массива и у верхнего значения.`,
81
+ },
82
+ "ConfigNode.file": {
83
+ en: `The path of the file the value was read from, e.g. \`"addons/amxmodx/configs/settings.yaml"\`; \`""\` for a text given to \`parse()\`.`,
84
+ ru: `Путь файла, из которого прочитано значение, например \`"addons/amxmodx/configs/settings.yaml"\`; \`""\` — для текста, отданного \`parse()\`.`,
85
+ },
86
+ "ConfigNode.format": {
87
+ en: `The file's format, one of \`"ini"\`, \`"yaml"\` or \`"json"\`.`,
88
+ ru: `Формат файла, одно из \`"ini"\`, \`"yaml"\` или \`"json"\`.`,
89
+ },
90
+ "ConfigNode.line": {
91
+ en: `The line the value - or its key - was read from, from \`1\`; \`0\` for a value set at run time.`,
92
+ ru: `Строка, из которой прочитано значение — или его ключ, — с \`1\`; \`0\` — для значения, заданного во время работы.`,
93
+ },
94
+ "ConfigNode.column": {
95
+ en: `The column the value - or its key - starts at, from \`1\`; \`0\` for a value set at run time and in an INI file.`,
96
+ ru: `Столбец, с которого начинается значение — или его ключ, — с \`1\`; \`0\` — для значения, заданного во время работы, и в INI-файле.`,
97
+ },
98
+ "ConfigNode.get": {
99
+ en: `The value a path leads to, e.g. \`"chat.prefix"\` or \`"items[0]"\`; \`null\` when there is none.`,
100
+ ru: `Значение, к которому ведёт путь, например \`"chat.prefix"\` или \`"items[0]"\`; \`null\`, если его нет.`,
101
+ },
102
+ "ConfigNode.has": {
103
+ en: `Whether a path leads to a value, \`null\` among them.`,
104
+ ru: `Ведёт ли путь к значению, \`null\` в том числе.`,
105
+ },
106
+ "ConfigNode.keys": {
107
+ en: `The keys of an object, in file order - of this one, or of the one a path leads to; [] for anything else.`,
108
+ ru: `Ключи объекта в порядке файла — этого или того, к которому ведёт путь; [] для всего остального.`,
109
+ },
110
+ "ConfigNode.values": {
111
+ en: `The items of an array or the values of an object, in file order - of this one, or of the one a path leads to; [] for anything else.`,
112
+ ru: `Элементы массива или значения объекта в порядке файла — этого или того, к которому ведёт путь; [] для всего остального.`,
113
+ },
114
+ "ConfigNode.getString": {
115
+ en: `A value as text: text as it is, a number as written, \`"true"\` or \`"false"\`; \`fallback\` (or \`""\`) for none, \`null\`, an object or an array.`,
116
+ ru: `Значение как текст: текст как есть, число как записано, \`"true"\` или \`"false"\`; \`fallback\` (или \`""\`) — если значения нет, для \`null\`, объекта и массива.`,
117
+ },
118
+ "ConfigNode.getNumber": {
119
+ en: `A value as a number: a number, or text that is one, e.g. \`"2.5"\`; \`fallback\` for anything else.`,
120
+ ru: `Значение как число: число или текст, который им является, например \`"2.5"\`; \`fallback\` для всего остального.`,
121
+ },
122
+ "ConfigNode.getBoolean": {
123
+ en: `A value as a boolean: \`true\` or \`false\`, a number (\`0\` is \`false\`), or text - \`"yes"\`, \`"no"\`, \`"on"\`, \`"off"\`, \`"true"\`, \`"false"\` in any case, or a number; \`fallback\` for anything else.`,
124
+ ru: `Значение как логическое: \`true\` или \`false\`, число (\`0\` — ложь) или текст — \`"yes"\`, \`"no"\`, \`"on"\`, \`"off"\`, \`"true"\`, \`"false"\` в любом регистре или число; \`fallback\` для всего остального.`,
125
+ },
126
+ "ConfigNode.getStrings": {
127
+ en: `A list of text: the text of each item of an array, or one value as a list of one; [] for none.`,
128
+ ru: `Список текста: текст каждого элемента массива или одно значение как список из одного; [] — если значения нет.`,
129
+ },
130
+ "ConfigNode.set": {
131
+ en: `Sets text at a path, making the objects - and the lists, before an item \`"[0]"\` - on the way; \`false\` where the path goes through a value that is not an object or a list.`,
132
+ ru: `Записывает текст по пути, создавая по дороге объекты — и списки перед элементом \`"[0]"\`; \`false\`, если путь идёт через значение, которое не объект и не список.`,
133
+ },
134
+ "ConfigNode.setNumber": {
135
+ en: `Sets a number at a path, as \`set()\` sets text.`,
136
+ ru: `Записывает число по пути, как \`set()\` записывает текст.`,
137
+ },
138
+ "ConfigNode.setBoolean": {
139
+ en: `Sets a boolean at a path, as \`set()\` sets text.`,
140
+ ru: `Записывает логическое значение по пути, как \`set()\` записывает текст.`,
141
+ },
142
+ "ConfigNode.setStrings": {
143
+ en: `Sets a list of text at a path, as \`set()\` sets text.`,
144
+ ru: `Записывает список текста по пути, как \`set()\` записывает текст.`,
145
+ },
146
+ "ConfigNode.remove": {
147
+ en: `Removes the value a path leads to; \`false\` when there was none.`,
148
+ ru: `Удаляет значение, к которому ведёт путь; \`false\`, если его не было.`,
149
+ },
150
+ "ConfigNode.save": {
151
+ en: `Writes the whole file back in its format, with the comments that were on lines of their own. \`false\` for a text given to \`parse()\`.`,
152
+ ru: `Записывает весь файл обратно в его формате, с комментариями, которые стояли на отдельных строках. \`false\` для текста, отданного \`parse()\`.`,
153
+ },
154
+ "resolve": {
155
+ en: `
156
+ The file a name is read from by \`read()\`: the name, when it ends in \`.ini\`,
157
+ \`.yaml\`, \`.yml\`, \`.json\` or \`.jsonc\`; else the first of \`name.ini\`, \`name.yaml\`,
158
+ \`name.yml\`, \`name.json\` and \`name.jsonc\` that is there - two of them are an
159
+ error in the server console - and \`name.yaml\` when none is.
160
+ `,
161
+ ru: `
162
+ Файл, из которого \`read()\` читает имя: само имя, если оно кончается на \`.ini\`,
163
+ \`.yaml\`, \`.yml\`, \`.json\` или \`.jsonc\`; иначе первый из \`name.ini\`, \`name.yaml\`,
164
+ \`name.yml\`, \`name.json\` и \`name.jsonc\`, который есть, — два из них — ошибка
165
+ в консоли сервера, — и \`name.yaml\`, если нет ни одного.
166
+ `,
167
+ },
168
+ "read": {
169
+ en: `
170
+ Reads a config file from \`configs/<baseDir>/\` - INI, YAML or JSON - as a
171
+ tree of values. A name without an extension finds the file (\`resolve()\`).
172
+ A file that is not there reads as an empty object, to be filled and saved;
173
+ one that cannot be read is reported in the server console with the line
174
+ and the column, and reads as an empty object too. For a file whose shape
175
+ is not known beforehand; a config of a known shape is read into an object
176
+ by \`load(name, defaults)\`.
177
+
178
+ const maps = configs.read("maps"); // maps.ini, .yaml, .yml, .json or .jsonc
179
+ for (const key of maps.keys()) console.log(key);
180
+ `,
181
+ ru: `
182
+ Читает файл конфига из \`configs/<baseDir>/\` — INI, YAML или JSON — как
183
+ дерево значений. Имя без расширения находит файл (\`resolve()\`). Файла нет —
184
+ читается пустой объект, чтобы его заполнить и сохранить; файл, который не
185
+ прочитать, называется в консоли сервера со строкой и столбцом и тоже
186
+ читается пустым объектом. Для файла, форма которого заранее неизвестна;
187
+ конфиг известной формы читается в объект через \`load(name, defaults)\`.
188
+
189
+ const maps = configs.read("maps"); // maps.ini, .yaml, .yml, .json или .jsonc
190
+ for (const key of maps.keys()) console.log(key);
191
+ `,
192
+ },
193
+ "parse": {
194
+ en: `Reads a text in a format - \`"ini"\`, \`"yaml"\` or \`"json"\` - as \`read()\` reads a file; \`save()\` has no file to write it to.`,
195
+ ru: `Читает текст в формате — \`"ini"\`, \`"yaml"\` или \`"json"\` — как \`read()\` читает файл; \`save()\` писать его некуда.`,
196
+ },
197
+ };
@@ -0,0 +1,20 @@
1
+ // The tooltips of src/types.ts, in both languages: scripts/apply-docs.ts writes the
2
+ // one AMXTS_DOCS_LANG picks into the JSDoc above each element.
3
+ export default {
4
+ "ConfigCoreOptions": {
5
+ en: `Config Core's options: \`configs\` in \`amxts.config.ts\`.`,
6
+ ru: `Настройки Config Core: \`configs\` в \`amxts.config.ts\`.`,
7
+ },
8
+ "ConfigCoreOptions.baseDir": {
9
+ en: `The folder under \`configs/\` that names are loaded from, e.g. \`"myserver"\`; \`""\` is \`configs/\` itself.`,
10
+ ru: `Папка внутри \`configs/\`, из которой загружаются файлы по имени, например \`"myserver"\`; \`""\` — сама \`configs/\`.`,
11
+ },
12
+ "ConfigFormat": {
13
+ en: `A config file's format, one of \`"ini"\`, \`"yaml"\` (\`.yaml\`, \`.yml\`) or \`"json"\` (\`.json\`, \`.jsonc\`).`,
14
+ ru: `Формат файла конфига, одно из \`"ini"\`, \`"yaml"\` (\`.yaml\`, \`.yml\`) или \`"json"\` (\`.json\`, \`.jsonc\`).`,
15
+ },
16
+ "ConfigKind": {
17
+ en: `A config value's kind, one of \`"object"\`, \`"array"\`, \`"string"\`, \`"number"\`, \`"boolean"\` or \`"null"\` - the kinds of JSON.`,
18
+ ru: `Вид значения конфига, одно из \`"object"\`, \`"array"\`, \`"string"\`, \`"number"\`, \`"boolean"\` или \`"null"\` — виды JSON.`,
19
+ },
20
+ };
package/src/files.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Where Config Core's files are: the folder under configs/ that names are
3
+ * relative to, and writing lines to a file. Not part of the API.
4
+ */
5
+ import * as fs from "~/fs";
6
+ import { EOL } from "~/os";
7
+ import { server } from "~/facade";
8
+
9
+ let baseDir = "";
10
+
11
+ /** Sets the folder under configs/ that names are relative to, e.g. "myserver"; "" is configs/ itself. */
12
+ export function setBaseDir(dir: string) {
13
+ baseDir = dir.trim();
14
+ }
15
+
16
+ /** A file's path: under configs/ and the base folder. */
17
+ export function configPath(file: string) {
18
+ const folder = baseDir.length > 0 ? `${server.configsDir}/${baseDir}` : server.configsDir;
19
+ return `${folder}/${file}`;
20
+ }
21
+
22
+ /** Lines to a file, its folder made when missing, each ended as this system ends them. */
23
+ export function writeLines(path: string, lines: string[]) {
24
+ const slash = path.lastIndexOf("/");
25
+ if (slash > 0) fs.mkdirSync(path.slice(0, slash), { recursive: true });
26
+ return fs.writeFileSync(path, lines.join(EOL) + EOL);
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Config Core — configs in INI, YAML or JSON for plugins: read into a typed
3
+ * object and written back, or read as a tree of values when the shape is not
4
+ * known beforehand. How to use it: README.md.
5
+ */
6
+ import * as fs from "~/fs";
7
+ import { ConfigFormat, ConfigKind, ConfigCoreOptions } from "./types";
8
+ import { TreeDocument, TreeNode } from "./internal";
9
+ import * as tree from "./tree";
10
+ import { YamlReader, writeYaml } from "./yaml";
11
+ import { JsonReader, writeJson } from "./json";
12
+ import { iniTree, writeIni } from "./ini-tree";
13
+ import { Config, readSections } from "./ini";
14
+ import * as files from "./files";
15
+
16
+ export * from "./types";
17
+
18
+ export default defineModule<ConfigCoreOptions>({
19
+ meta: { name: "config-core", configKey: "configs" },
20
+ imports: [{ from: "@amxts/config-core", as: "configs" }],
21
+ defaults: { baseDir: "" },
22
+ setup(options) {
23
+ setBaseDir(options.baseDir);
24
+ },
25
+ });
26
+
27
+ /** Sets the folder under `configs/` that file names are relative to, e.g. `"myserver"`; `""` is `configs/` itself. */
28
+ export function setBaseDir(dir: string) {
29
+ files.setBaseDir(dir);
30
+ }
31
+
32
+ /**
33
+ * Reads a config file into an object shaped like `defaults`: each value the
34
+ * file has, where it is of the right kind, and the default for the rest.
35
+ * The file is `configs/<baseDir>/<name>` - YAML, JSON or INI, whichever is
36
+ * there (`resolve()`). A value of the wrong kind, a name not in its union and
37
+ * a key the object does not have are said in the server console with the
38
+ * file, the line and the column. `save()` writes the object back.
39
+ *
40
+ * const settings = configs.load("settings", {
41
+ * chat: { prefix: "[HNS]" },
42
+ * round: { time: 2.5 },
43
+ * });
44
+ * settings.round.time = 3;
45
+ * configs.save(settings);
46
+ */
47
+ export function load<T extends object>(name: string, defaults: T): T;
48
+ // The build turns every call with defaults into a reader for the object's
49
+ // shape (the core's scripts/typed-configs.ts), and save() into one that finds
50
+ // the object's file: these bodies are what the compiler sees of the two, and
51
+ // are reached only by a call the build could not read.
52
+ export function load(name: string) {
53
+ console.error(`[ConfigCore] configs.load("${name}") has no defaults - a config is read into an object: configs.load(name, defaults)`);
54
+ return parse("", "yaml");
55
+ }
56
+
57
+ /**
58
+ * Writes an object `load()` read back into its file, in the file's format:
59
+ * comments on lines of their own stay where they were. `false` when it could
60
+ * not be written, or the object was not read by `load()`.
61
+ */
62
+ export function save<T extends object>(settings: T): boolean;
63
+ export function save(_settings: ConfigNode) {
64
+ console.error("[ConfigCore] configs.save(): this object was not read by configs.load() - nothing is written");
65
+ return false;
66
+ }
67
+
68
+ /** The extensions a config file has, in the order a name without one looks for them. */
69
+ const EXTENSIONS = [".ini", ".yaml", ".yml", ".json", ".jsonc"];
70
+
71
+ function formatOf(file: string) {
72
+ const lower = file.toLowerCase();
73
+ if (lower.endsWith(".yaml") || lower.endsWith(".yml")) return "yaml";
74
+ if (lower.endsWith(".json") || lower.endsWith(".jsonc")) return "json";
75
+ return "ini";
76
+ }
77
+
78
+ function report(file: string, line: number, column: number, message: string) {
79
+ console.error(`[ConfigCore] ${file.length > 0 ? file : "text"}:${line}:${column}: ${message}`);
80
+ }
81
+
82
+ /** A text read into a tree in its format; what cannot be read is reported with its place, and leaves an empty tree. */
83
+ function parseDocument(text: string, format: ConfigFormat, file: string) {
84
+ const clean = tree.normalize(text);
85
+ const document: TreeDocument = { file, format, root: tree.makeNode("object", "", "", { line: 1, column: 1 }), indent: "", tail: null };
86
+
87
+ if (format == "ini") {
88
+ const config: Config = { name: file, sections: [] };
89
+ readSections(config, clean);
90
+ document.root = iniTree(config);
91
+ return document;
92
+ }
93
+
94
+ if (format == "yaml") {
95
+ const reader = new YamlReader(clean);
96
+ const root = reader.read();
97
+ const place = reader.placeAt(reader.errorAt);
98
+ if (reader.failed) report(file, place.line, place.column, reader.error);
99
+ else document.root = root;
100
+ document.indent = reader.indent;
101
+ document.tail = reader.takeComments();
102
+ return document;
103
+ }
104
+
105
+ const reader = new JsonReader(clean);
106
+ const root = reader.read();
107
+ const place = reader.placeAt(reader.errorAt);
108
+ if (reader.failed) report(file, place.line, place.column, reader.error);
109
+ else document.root = root;
110
+ document.indent = reader.indent;
111
+ document.tail = reader.takeComments();
112
+ return document;
113
+ }
114
+
115
+ function writeDocument(document: TreeDocument) {
116
+ if (document.format == "yaml") return writeYaml(document);
117
+ if (document.format == "json") return writeJson(document);
118
+ return writeIni(document);
119
+ }
120
+
121
+ /** A document read, and the ConfigNodes that show its values: one a value, made when it is first asked for. */
122
+ interface Loaded {
123
+ document: TreeDocument;
124
+ shown: Map<TreeNode, ConfigNode>;
125
+ }
126
+
127
+ function nodeOf(node: TreeNode, loaded: Loaded) {
128
+ if (loaded.shown.has(node)) return loaded.shown.get(node);
129
+ const made = new ConfigNode(node, loaded);
130
+ loaded.shown.set(node, made);
131
+ return made;
132
+ }
133
+
134
+ function loadedOf(document: TreeDocument) {
135
+ const loaded: Loaded = { document, shown: new Map<TreeNode, ConfigNode>() };
136
+ return nodeOf(document.root, loaded);
137
+ }
138
+
139
+ /**
140
+ * The container a path goes on through: the member there, or a new one made
141
+ * for it - a list when the next part is an item, "[0]", an object otherwise.
142
+ * Null where the path cannot go.
143
+ */
144
+ function containerFor(parent: TreeNode, part: string, next: string, format: ConfigFormat) {
145
+ const found = tree.member(parent, part);
146
+ if (found != null) return found.kind == "object" || found.kind == "array" ? found : null;
147
+ const index = tree.itemIndex(part);
148
+ if (parent.kind == "array" ? index != parent.items.length : index >= 0) return null;
149
+ const made = tree.makeNode(tree.itemIndex(next) >= 0 ? "array" : "object", parent.kind == "object" ? part : "", "", tree.nowhere());
150
+ made.foldCase = format == "ini" && made.kind == "object";
151
+ parent.items.push(made);
152
+ return made;
153
+ }
154
+
155
+ /** Puts a value at a member or an item, in the place and with the comments of the one it replaces. */
156
+ function putNode(parent: TreeNode, part: string, made: TreeNode) {
157
+ let index = -1;
158
+ if (parent.kind == "object" && tree.itemIndex(part) < 0) index = tree.memberIndex(parent, part);
159
+ else if (parent.kind == "array" && tree.itemIndex(part) >= 0) index = tree.itemIndex(part);
160
+ else return false;
161
+
162
+ if (index >= 0 && index < parent.items.length) {
163
+ const old = parent.items[index];
164
+ made.key = old.key;
165
+ made.comments = old.comments;
166
+ made.block = old.block;
167
+ made.line = old.line;
168
+ made.column = old.column;
169
+ parent.items[index] = made;
170
+ return true;
171
+ }
172
+
173
+ if (parent.kind == "array" && index != parent.items.length) return false;
174
+ made.key = parent.kind == "object" ? part : "";
175
+ parent.items.push(made);
176
+ return true;
177
+ }
178
+
179
+ /**
180
+ * A value of a config file: an object, an array, text, a number, a boolean
181
+ * or `null`, with the place it was read from. `read()` gives the file's top
182
+ * value; a path leads into it: `"chat.prefix"`, `"items[0].name"`. For a file
183
+ * whose shape is not known beforehand; a config of a known shape is read
184
+ * into an object by `load(name, defaults)`.
185
+ *
186
+ * const maps = configs.read("maps");
187
+ * for (const map of maps.values()) if (map.getBoolean("enabled")) console.log(map.key);
188
+ */
189
+ export class ConfigNode {
190
+ /** The value's kind, one of `"object"`, `"array"`, `"string"`, `"number"`, `"boolean"` or `"null"`. */
191
+ readonly kind: ConfigKind;
192
+ /** The value's key in its object, e.g. `"prefix"`; `""` for an item of an array and for the top value. */
193
+ readonly key: string;
194
+ /** The path of the file the value was read from, e.g. `"addons/amxmodx/configs/settings.yaml"`; `""` for a text given to `parse()`. */
195
+ readonly file: string;
196
+ /** The file's format, one of `"ini"`, `"yaml"` or `"json"`. */
197
+ readonly format: ConfigFormat;
198
+ /** The line the value - or its key - was read from, from `1`; `0` for a value set at run time. */
199
+ readonly line: number;
200
+ /** The column the value - or its key - starts at, from `1`; `0` for a value set at run time and in an INI file. */
201
+ readonly column: number;
202
+
203
+ constructor(private node: TreeNode, private loaded: Loaded) {
204
+ this.kind = node.kind;
205
+ this.key = node.key;
206
+ this.file = loaded.document.file;
207
+ this.format = loaded.document.format;
208
+ this.line = node.line;
209
+ this.column = node.column;
210
+ }
211
+
212
+ /** The value a path leads to, e.g. `"chat.prefix"` or `"items[0]"`; `null` when there is none. */
213
+ get(path: string) {
214
+ const found = tree.follow(this.node, path);
215
+ return found != null ? nodeOf(found, this.loaded) : null;
216
+ }
217
+
218
+ /** Whether a path leads to a value, `null` among them. */
219
+ has(path: string) {
220
+ return tree.follow(this.node, path) != null;
221
+ }
222
+
223
+ /** The keys of an object, in file order - of this one, or of the one a path leads to; [] for anything else. */
224
+ keys(path?: string) {
225
+ const found = tree.follow(this.node, path ?? "");
226
+ const keys: string[] = [];
227
+ if (found != null && found.kind == "object") found.items.forEach(each => keys.push(each.key));
228
+ return keys;
229
+ }
230
+
231
+ /** The items of an array or the values of an object, in file order - of this one, or of the one a path leads to; [] for anything else. */
232
+ values(path?: string) {
233
+ const found = tree.follow(this.node, path ?? "");
234
+ const values: ConfigNode[] = [];
235
+ if (found == null || (found.kind != "object" && found.kind != "array")) return values;
236
+ for (const each of found.items) values.push(nodeOf(each, this.loaded));
237
+ return values;
238
+ }
239
+
240
+ /** A value as text: text as it is, a number as written, `"true"` or `"false"`; `fallback` (or `""`) for none, `null`, an object or an array. */
241
+ getString(path?: string, fallback?: string) {
242
+ return tree.scalarText(tree.follow(this.node, path ?? "")) ?? fallback ?? "";
243
+ }
244
+
245
+ /** A value as a number: a number, or text that is one, e.g. `"2.5"`; `fallback` for anything else. */
246
+ getNumber(path?: string, fallback = 0) {
247
+ const value = tree.numberOf(tree.follow(this.node, path ?? ""));
248
+ return isNaN(value) ? fallback : value;
249
+ }
250
+
251
+ /** A value as a boolean: `true` or `false`, a number (`0` is `false`), or text - `"yes"`, `"no"`, `"on"`, `"off"`, `"true"`, `"false"` in any case, or a number; `fallback` for anything else. */
252
+ getBoolean(path?: string, fallback = false) {
253
+ const value = tree.booleanOf(tree.follow(this.node, path ?? ""));
254
+ return value < 0 ? fallback : value == 1;
255
+ }
256
+
257
+ /** A list of text: the text of each item of an array, or one value as a list of one; [] for none. */
258
+ getStrings(path?: string) {
259
+ const found = tree.follow(this.node, path ?? "");
260
+ const list: string[] = [];
261
+ if (found == null) return list;
262
+ if (tree.isScalar(found)) list.push(found.text);
263
+ if (found.kind == "array") found.items.filter(tree.isScalar).forEach(item => list.push(item.text));
264
+ return list;
265
+ }
266
+
267
+ /** Sets text at a path, making the objects - and the lists, before an item `"[0]"` - on the way; `false` where the path goes through a value that is not an object or a list. */
268
+ set(path: string, value: string) {
269
+ return this.put(path, tree.makeNode("string", "", value, tree.nowhere()));
270
+ }
271
+
272
+ /** Sets a number at a path, as `set()` sets text. */
273
+ setNumber(path: string, value: number) {
274
+ return this.put(path, tree.numberNode("", value, tree.nowhere()));
275
+ }
276
+
277
+ /** Sets a boolean at a path, as `set()` sets text. */
278
+ setBoolean(path: string, value: boolean) {
279
+ return this.put(path, tree.makeNode("boolean", "", value ? "true" : "false", tree.nowhere()));
280
+ }
281
+
282
+ /** Sets a list of text at a path, as `set()` sets text. */
283
+ setStrings(path: string, values: string[]) {
284
+ const list = tree.makeNode("array", "", "", tree.nowhere());
285
+ for (const value of values) list.items.push(tree.makeNode("string", "", value, tree.nowhere()));
286
+ return this.put(path, list);
287
+ }
288
+
289
+ /** Removes the value a path leads to; `false` when there was none. */
290
+ remove(path: string) {
291
+ const parts = tree.pathParts(path);
292
+ if (parts.length == 0) return false;
293
+ const parent = tree.follow(this.node, parts.slice(0, -1).join("."));
294
+ const last = parts[parts.length - 1];
295
+ if (parent == null) return false;
296
+ let index = -1;
297
+ if (parent.kind == "object" && tree.itemIndex(last) < 0) index = tree.memberIndex(parent, last);
298
+ else if (parent.kind == "array") index = tree.itemIndex(last);
299
+ if (index < 0 || index >= parent.items.length) return false;
300
+ parent.items.splice(index, 1);
301
+ return true;
302
+ }
303
+
304
+ /** Writes the whole file back in its format, with the comments that were on lines of their own. `false` for a text given to `parse()`. */
305
+ save() {
306
+ const document = this.loaded.document;
307
+ if (document.file.length == 0) return false;
308
+ return files.writeLines(document.file, writeDocument(document));
309
+ }
310
+
311
+ private put(path: string, made: TreeNode) {
312
+ const parts = tree.pathParts(path);
313
+ if (parts.length == 0) return false;
314
+ let parent = this.node;
315
+ for (let i = 0; i < parts.length - 1; i++) {
316
+ const next = containerFor(parent, parts[i], parts[i + 1], this.loaded.document.format);
317
+ if (next == null) return false;
318
+ parent = next;
319
+ }
320
+ return putNode(parent, parts[parts.length - 1], made);
321
+ }
322
+ }
323
+
324
+ /**
325
+ * The file a name is read from by `read()`: the name, when it ends in `.ini`,
326
+ * `.yaml`, `.yml`, `.json` or `.jsonc`; else the first of `name.ini`, `name.yaml`,
327
+ * `name.yml`, `name.json` and `name.jsonc` that is there - two of them are an
328
+ * error in the server console - and `name.yaml` when none is.
329
+ */
330
+ export function resolve(name: string) {
331
+ const file = name.trim();
332
+ if (EXTENSIONS.some(extension => file.toLowerCase().endsWith(extension))) return file;
333
+ const found = EXTENSIONS.map(extension => `${file}${extension}`).filter(each => fs.existsSync(files.configPath(each)));
334
+ if (found.length > 1) console.error(`[ConfigCore] ${files.configPath(file)}: ${found.join(", ")} are all there - ${found[0]} is read; keep one of them`);
335
+ return found.length > 0 ? found[0] : `${file}.yaml`;
336
+ }
337
+
338
+ /**
339
+ * Reads a config file from `configs/<baseDir>/` - INI, YAML or JSON - as a
340
+ * tree of values. A name without an extension finds the file (`resolve()`).
341
+ * A file that is not there reads as an empty object, to be filled and saved;
342
+ * one that cannot be read is reported in the server console with the line
343
+ * and the column, and reads as an empty object too. For a file whose shape
344
+ * is not known beforehand; a config of a known shape is read into an object
345
+ * by `load(name, defaults)`.
346
+ *
347
+ * const maps = configs.read("maps"); // maps.ini, .yaml, .yml, .json or .jsonc
348
+ * for (const key of maps.keys()) console.log(key);
349
+ */
350
+ export function read(name: string) {
351
+ const file = resolve(name);
352
+ const path = files.configPath(file);
353
+ const text = fs.readFileSync(path);
354
+ return loadedOf(parseDocument(text ?? "", formatOf(file), path));
355
+ }
356
+
357
+ /** Reads a text in a format - `"ini"`, `"yaml"` or `"json"` - as `read()` reads a file; `save()` has no file to write it to. */
358
+ export function parse(text: string, format: ConfigFormat) {
359
+ return loadedOf(parseDocument(text, format, ""));
360
+ }