opticore-webapp-core 1.0.21 → 1.0.23
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/dist/index.cjs +68 -11
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +65 -8
- package/package.json +7 -7
- package/src/utils/parsing/parsingYaml.utils.ts +70 -10
- package/test/regression.test.mjs +76 -0
package/dist/index.cjs
CHANGED
|
@@ -546,7 +546,7 @@ var CLogLevel = {
|
|
|
546
546
|
};
|
|
547
547
|
|
|
548
548
|
// src/utils/parsing/parsingYaml.utils.ts
|
|
549
|
-
var
|
|
549
|
+
var import_fs = require("fs");
|
|
550
550
|
var import_opticore_http_response4 = require("opticore-http-response");
|
|
551
551
|
var import_opticore_logger2 = require("opticore-logger");
|
|
552
552
|
var import_opticore_translator4 = require("opticore-translator");
|
|
@@ -586,10 +586,21 @@ var YamlParsing = class {
|
|
|
586
586
|
absolutPath() {
|
|
587
587
|
return import_path2.default.join(process.cwd(), "src", "utils", "translations");
|
|
588
588
|
}
|
|
589
|
-
|
|
589
|
+
/**
|
|
590
|
+
* Reads and parses a YAML config file (relative to the process working directory) and RETURNS its content, so that
|
|
591
|
+
* it can be passed straight to a consumer:
|
|
592
|
+
*
|
|
593
|
+
* new WebServer({ corsOriginOptions: yamlParsing.readFile(environment.corsOptions), ... })
|
|
594
|
+
*
|
|
595
|
+
* It used to parse the file and throw the result away (it returned `Promise<void>`), so such a call handed the
|
|
596
|
+
* consumer a Promise, and CORS silently ran with its defaults: any origin, no credentials.
|
|
597
|
+
*
|
|
598
|
+
* It is synchronous on purpose: a configuration is needed before the server starts. An unreadable or malformed
|
|
599
|
+
* file is logged and yields `{}`; callers that cannot run without a value must check for it.
|
|
600
|
+
*/
|
|
601
|
+
readFile(filePath) {
|
|
590
602
|
try {
|
|
591
|
-
|
|
592
|
-
await this.parsing(yamlContent);
|
|
603
|
+
return this.parsing((0, import_fs.readFileSync)(filePath, "utf-8"));
|
|
593
604
|
} catch (error) {
|
|
594
605
|
this.logger.error({
|
|
595
606
|
message: error.message,
|
|
@@ -598,14 +609,50 @@ var YamlParsing = class {
|
|
|
598
609
|
stackTrace: error.stack,
|
|
599
610
|
httpCodeValue: import_opticore_http_response4.HttpStatusCode.NOT_ACCEPTABLE
|
|
600
611
|
});
|
|
612
|
+
return {};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* A YAML scalar as a typed value: `true` / `false` become booleans, `null` / `~` / empty become null,
|
|
617
|
+
* numbers become numbers, quoted text becomes the text inside the quotes, anything else the trimmed string.
|
|
618
|
+
*
|
|
619
|
+
* @param raw
|
|
620
|
+
* @private
|
|
621
|
+
*/
|
|
622
|
+
scalar(raw) {
|
|
623
|
+
const value = (raw ?? "").trim();
|
|
624
|
+
if (value === "" || value === "~" || value === "null") return null;
|
|
625
|
+
if (value === "true") return true;
|
|
626
|
+
if (value === "false") return false;
|
|
627
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
|
|
628
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
629
|
+
return value.slice(1, -1);
|
|
630
|
+
}
|
|
631
|
+
return value;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* The value after `key:` — a scalar, or an inline list (`[a, b]`, `[]`).
|
|
635
|
+
*
|
|
636
|
+
* @param raw
|
|
637
|
+
* @private
|
|
638
|
+
*/
|
|
639
|
+
value(raw) {
|
|
640
|
+
const value = (raw ?? "").replace(/\s+#.*$/, "").trim();
|
|
641
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
642
|
+
const inner = value.slice(1, -1).trim();
|
|
643
|
+
return inner === "" ? [] : inner.split(",").map((item) => this.scalar(item));
|
|
601
644
|
}
|
|
645
|
+
return this.scalar(value);
|
|
602
646
|
}
|
|
603
647
|
/**
|
|
648
|
+
* Parses the supported YAML subset: `key: value` pairs (typed scalars), inline lists (`[a, b]`), block lists
|
|
649
|
+
* (`- item` lines under a key) and one level of nested `key: value` pairs. Full-line and trailing comments,
|
|
650
|
+
* and blank lines, are ignored.
|
|
604
651
|
*
|
|
605
652
|
* @param content
|
|
606
653
|
* @private
|
|
607
654
|
*/
|
|
608
|
-
|
|
655
|
+
parsing(content) {
|
|
609
656
|
const result = {};
|
|
610
657
|
const lines = content.split("\n");
|
|
611
658
|
let currentKey = null;
|
|
@@ -613,15 +660,25 @@ var YamlParsing = class {
|
|
|
613
660
|
if (!line.trim() || line.trim().startsWith("#")) {
|
|
614
661
|
continue;
|
|
615
662
|
}
|
|
663
|
+
const listItemMatch = line.match(/^\s*-\s+(.*)$/);
|
|
664
|
+
if (listItemMatch && currentKey) {
|
|
665
|
+
if (!Array.isArray(result[currentKey])) {
|
|
666
|
+
result[currentKey] = [];
|
|
667
|
+
}
|
|
668
|
+
result[currentKey].push(this.scalar(listItemMatch[1].replace(/\s+#.*$/, "")));
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
616
671
|
const keyValueMatch = line.match(/^(\s*)([a-zA-Z0-9_]+):(?:\s*(.*))?$/);
|
|
617
672
|
if (keyValueMatch) {
|
|
618
673
|
const [, indent, key, value] = keyValueMatch;
|
|
619
674
|
if (indent.length > 0 && currentKey) {
|
|
620
|
-
result[currentKey]
|
|
621
|
-
|
|
675
|
+
if (result[currentKey] === null || typeof result[currentKey] !== "object" || Array.isArray(result[currentKey])) {
|
|
676
|
+
result[currentKey] = {};
|
|
677
|
+
}
|
|
678
|
+
result[currentKey][key] = this.value(value);
|
|
622
679
|
} else {
|
|
623
680
|
currentKey = key;
|
|
624
|
-
result[key] = value
|
|
681
|
+
result[key] = this.value(value);
|
|
625
682
|
}
|
|
626
683
|
} else {
|
|
627
684
|
this.logger.error({
|
|
@@ -656,7 +713,7 @@ var import_node_process = __toESM(require("process"), 1);
|
|
|
656
713
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
657
714
|
var import_ansi_colors = __toESM(require("ansi-colors"), 1);
|
|
658
715
|
var import_path3 = __toESM(require("path"), 1);
|
|
659
|
-
var
|
|
716
|
+
var import_fs2 = __toESM(require("fs"), 1);
|
|
660
717
|
var import_opticore_translator5 = require("opticore-translator");
|
|
661
718
|
var Utility = class {
|
|
662
719
|
localLang;
|
|
@@ -733,8 +790,8 @@ var Utility = class {
|
|
|
733
790
|
*/
|
|
734
791
|
getEnvFileLoading(filePath) {
|
|
735
792
|
const fullPath = import_path3.default.resolve(import_node_process.default.cwd(), filePath);
|
|
736
|
-
if (
|
|
737
|
-
const env =
|
|
793
|
+
if (import_fs2.default.existsSync(fullPath)) {
|
|
794
|
+
const env = import_fs2.default.readFileSync(fullPath, "utf-8");
|
|
738
795
|
const lines = env.split("\n");
|
|
739
796
|
lines.forEach((line) => {
|
|
740
797
|
const match = line.match(/^([^#=]+)=([^#]+)$/);
|
package/dist/index.d.cts
CHANGED
|
@@ -135,8 +135,38 @@ declare class YamlParsing {
|
|
|
135
135
|
private readonly localeLanguage;
|
|
136
136
|
constructor(localeLanguage: string, environmentPath: any);
|
|
137
137
|
absolutPath(): string;
|
|
138
|
-
readFile(filePath: string): Promise<void>;
|
|
139
138
|
/**
|
|
139
|
+
* Reads and parses a YAML config file (relative to the process working directory) and RETURNS its content, so that
|
|
140
|
+
* it can be passed straight to a consumer:
|
|
141
|
+
*
|
|
142
|
+
* new WebServer({ corsOriginOptions: yamlParsing.readFile(environment.corsOptions), ... })
|
|
143
|
+
*
|
|
144
|
+
* It used to parse the file and throw the result away (it returned `Promise<void>`), so such a call handed the
|
|
145
|
+
* consumer a Promise, and CORS silently ran with its defaults: any origin, no credentials.
|
|
146
|
+
*
|
|
147
|
+
* It is synchronous on purpose: a configuration is needed before the server starts. An unreadable or malformed
|
|
148
|
+
* file is logged and yields `{}`; callers that cannot run without a value must check for it.
|
|
149
|
+
*/
|
|
150
|
+
readFile(filePath: string): Record<string, any>;
|
|
151
|
+
/**
|
|
152
|
+
* A YAML scalar as a typed value: `true` / `false` become booleans, `null` / `~` / empty become null,
|
|
153
|
+
* numbers become numbers, quoted text becomes the text inside the quotes, anything else the trimmed string.
|
|
154
|
+
*
|
|
155
|
+
* @param raw
|
|
156
|
+
* @private
|
|
157
|
+
*/
|
|
158
|
+
private scalar;
|
|
159
|
+
/**
|
|
160
|
+
* The value after `key:` — a scalar, or an inline list (`[a, b]`, `[]`).
|
|
161
|
+
*
|
|
162
|
+
* @param raw
|
|
163
|
+
* @private
|
|
164
|
+
*/
|
|
165
|
+
private value;
|
|
166
|
+
/**
|
|
167
|
+
* Parses the supported YAML subset: `key: value` pairs (typed scalars), inline lists (`[a, b]`), block lists
|
|
168
|
+
* (`- item` lines under a key) and one level of nested `key: value` pairs. Full-line and trailing comments,
|
|
169
|
+
* and blank lines, are ignored.
|
|
140
170
|
*
|
|
141
171
|
* @param content
|
|
142
172
|
* @private
|
package/dist/index.d.ts
CHANGED
|
@@ -135,8 +135,38 @@ declare class YamlParsing {
|
|
|
135
135
|
private readonly localeLanguage;
|
|
136
136
|
constructor(localeLanguage: string, environmentPath: any);
|
|
137
137
|
absolutPath(): string;
|
|
138
|
-
readFile(filePath: string): Promise<void>;
|
|
139
138
|
/**
|
|
139
|
+
* Reads and parses a YAML config file (relative to the process working directory) and RETURNS its content, so that
|
|
140
|
+
* it can be passed straight to a consumer:
|
|
141
|
+
*
|
|
142
|
+
* new WebServer({ corsOriginOptions: yamlParsing.readFile(environment.corsOptions), ... })
|
|
143
|
+
*
|
|
144
|
+
* It used to parse the file and throw the result away (it returned `Promise<void>`), so such a call handed the
|
|
145
|
+
* consumer a Promise, and CORS silently ran with its defaults: any origin, no credentials.
|
|
146
|
+
*
|
|
147
|
+
* It is synchronous on purpose: a configuration is needed before the server starts. An unreadable or malformed
|
|
148
|
+
* file is logged and yields `{}`; callers that cannot run without a value must check for it.
|
|
149
|
+
*/
|
|
150
|
+
readFile(filePath: string): Record<string, any>;
|
|
151
|
+
/**
|
|
152
|
+
* A YAML scalar as a typed value: `true` / `false` become booleans, `null` / `~` / empty become null,
|
|
153
|
+
* numbers become numbers, quoted text becomes the text inside the quotes, anything else the trimmed string.
|
|
154
|
+
*
|
|
155
|
+
* @param raw
|
|
156
|
+
* @private
|
|
157
|
+
*/
|
|
158
|
+
private scalar;
|
|
159
|
+
/**
|
|
160
|
+
* The value after `key:` — a scalar, or an inline list (`[a, b]`, `[]`).
|
|
161
|
+
*
|
|
162
|
+
* @param raw
|
|
163
|
+
* @private
|
|
164
|
+
*/
|
|
165
|
+
private value;
|
|
166
|
+
/**
|
|
167
|
+
* Parses the supported YAML subset: `key: value` pairs (typed scalars), inline lists (`[a, b]`), block lists
|
|
168
|
+
* (`- item` lines under a key) and one level of nested `key: value` pairs. Full-line and trailing comments,
|
|
169
|
+
* and blank lines, are ignored.
|
|
140
170
|
*
|
|
141
171
|
* @param content
|
|
142
172
|
* @private
|
package/dist/index.js
CHANGED
|
@@ -501,7 +501,7 @@ var CLogLevel = {
|
|
|
501
501
|
};
|
|
502
502
|
|
|
503
503
|
// src/utils/parsing/parsingYaml.utils.ts
|
|
504
|
-
import {
|
|
504
|
+
import { readFileSync } from "fs";
|
|
505
505
|
import { HttpStatusCode as HttpStatusCode3 } from "opticore-http-response";
|
|
506
506
|
import { LoggerCore as LoggerCore2 } from "opticore-logger";
|
|
507
507
|
import { TranslationLoader as TranslationLoader4 } from "opticore-translator";
|
|
@@ -541,10 +541,21 @@ var YamlParsing = class {
|
|
|
541
541
|
absolutPath() {
|
|
542
542
|
return path2.join(process.cwd(), "src", "utils", "translations");
|
|
543
543
|
}
|
|
544
|
-
|
|
544
|
+
/**
|
|
545
|
+
* Reads and parses a YAML config file (relative to the process working directory) and RETURNS its content, so that
|
|
546
|
+
* it can be passed straight to a consumer:
|
|
547
|
+
*
|
|
548
|
+
* new WebServer({ corsOriginOptions: yamlParsing.readFile(environment.corsOptions), ... })
|
|
549
|
+
*
|
|
550
|
+
* It used to parse the file and throw the result away (it returned `Promise<void>`), so such a call handed the
|
|
551
|
+
* consumer a Promise, and CORS silently ran with its defaults: any origin, no credentials.
|
|
552
|
+
*
|
|
553
|
+
* It is synchronous on purpose: a configuration is needed before the server starts. An unreadable or malformed
|
|
554
|
+
* file is logged and yields `{}`; callers that cannot run without a value must check for it.
|
|
555
|
+
*/
|
|
556
|
+
readFile(filePath) {
|
|
545
557
|
try {
|
|
546
|
-
|
|
547
|
-
await this.parsing(yamlContent);
|
|
558
|
+
return this.parsing(readFileSync(filePath, "utf-8"));
|
|
548
559
|
} catch (error) {
|
|
549
560
|
this.logger.error({
|
|
550
561
|
message: error.message,
|
|
@@ -553,14 +564,50 @@ var YamlParsing = class {
|
|
|
553
564
|
stackTrace: error.stack,
|
|
554
565
|
httpCodeValue: HttpStatusCode3.NOT_ACCEPTABLE
|
|
555
566
|
});
|
|
567
|
+
return {};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* A YAML scalar as a typed value: `true` / `false` become booleans, `null` / `~` / empty become null,
|
|
572
|
+
* numbers become numbers, quoted text becomes the text inside the quotes, anything else the trimmed string.
|
|
573
|
+
*
|
|
574
|
+
* @param raw
|
|
575
|
+
* @private
|
|
576
|
+
*/
|
|
577
|
+
scalar(raw) {
|
|
578
|
+
const value = (raw ?? "").trim();
|
|
579
|
+
if (value === "" || value === "~" || value === "null") return null;
|
|
580
|
+
if (value === "true") return true;
|
|
581
|
+
if (value === "false") return false;
|
|
582
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
|
|
583
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
584
|
+
return value.slice(1, -1);
|
|
585
|
+
}
|
|
586
|
+
return value;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* The value after `key:` — a scalar, or an inline list (`[a, b]`, `[]`).
|
|
590
|
+
*
|
|
591
|
+
* @param raw
|
|
592
|
+
* @private
|
|
593
|
+
*/
|
|
594
|
+
value(raw) {
|
|
595
|
+
const value = (raw ?? "").replace(/\s+#.*$/, "").trim();
|
|
596
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
597
|
+
const inner = value.slice(1, -1).trim();
|
|
598
|
+
return inner === "" ? [] : inner.split(",").map((item) => this.scalar(item));
|
|
556
599
|
}
|
|
600
|
+
return this.scalar(value);
|
|
557
601
|
}
|
|
558
602
|
/**
|
|
603
|
+
* Parses the supported YAML subset: `key: value` pairs (typed scalars), inline lists (`[a, b]`), block lists
|
|
604
|
+
* (`- item` lines under a key) and one level of nested `key: value` pairs. Full-line and trailing comments,
|
|
605
|
+
* and blank lines, are ignored.
|
|
559
606
|
*
|
|
560
607
|
* @param content
|
|
561
608
|
* @private
|
|
562
609
|
*/
|
|
563
|
-
|
|
610
|
+
parsing(content) {
|
|
564
611
|
const result = {};
|
|
565
612
|
const lines = content.split("\n");
|
|
566
613
|
let currentKey = null;
|
|
@@ -568,15 +615,25 @@ var YamlParsing = class {
|
|
|
568
615
|
if (!line.trim() || line.trim().startsWith("#")) {
|
|
569
616
|
continue;
|
|
570
617
|
}
|
|
618
|
+
const listItemMatch = line.match(/^\s*-\s+(.*)$/);
|
|
619
|
+
if (listItemMatch && currentKey) {
|
|
620
|
+
if (!Array.isArray(result[currentKey])) {
|
|
621
|
+
result[currentKey] = [];
|
|
622
|
+
}
|
|
623
|
+
result[currentKey].push(this.scalar(listItemMatch[1].replace(/\s+#.*$/, "")));
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
571
626
|
const keyValueMatch = line.match(/^(\s*)([a-zA-Z0-9_]+):(?:\s*(.*))?$/);
|
|
572
627
|
if (keyValueMatch) {
|
|
573
628
|
const [, indent, key, value] = keyValueMatch;
|
|
574
629
|
if (indent.length > 0 && currentKey) {
|
|
575
|
-
result[currentKey]
|
|
576
|
-
|
|
630
|
+
if (result[currentKey] === null || typeof result[currentKey] !== "object" || Array.isArray(result[currentKey])) {
|
|
631
|
+
result[currentKey] = {};
|
|
632
|
+
}
|
|
633
|
+
result[currentKey][key] = this.value(value);
|
|
577
634
|
} else {
|
|
578
635
|
currentKey = key;
|
|
579
|
-
result[key] = value
|
|
636
|
+
result[key] = this.value(value);
|
|
580
637
|
}
|
|
581
638
|
} else {
|
|
582
639
|
this.logger.error({
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opticore-webapp-core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
4
4
|
"description": "opticore Web Application core module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "
|
|
10
|
+
"test": "node --test test/*.test.mjs",
|
|
11
11
|
"build": "tsup"
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"dotenv": "^16.5.0",
|
|
31
31
|
"mongodb": "^6.6.1",
|
|
32
32
|
"mysql": "^2.18.1",
|
|
33
|
-
"opticore-catch-exception-error": "^1.0.
|
|
34
|
-
"opticore-env-access": "^1.0.
|
|
33
|
+
"opticore-catch-exception-error": "^1.0.31",
|
|
34
|
+
"opticore-env-access": "^1.0.27",
|
|
35
35
|
"opticore-express": "^1.0.9",
|
|
36
|
-
"opticore-http-response": "^1.0.
|
|
37
|
-
"opticore-logger": "^1.0.
|
|
38
|
-
"opticore-translator": "^1.0.
|
|
36
|
+
"opticore-http-response": "^1.0.12",
|
|
37
|
+
"opticore-logger": "^1.0.35",
|
|
38
|
+
"opticore-translator": "^1.0.17",
|
|
39
39
|
"pg": "^8.15.6"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
2
|
import { HttpStatusCode } from "opticore-http-response";
|
|
3
3
|
import { ILoggerConfig, LoggerCore } from "opticore-logger";
|
|
4
4
|
import { TranslationLoader } from "opticore-translator";
|
|
@@ -22,10 +22,21 @@ export class YamlParsing {
|
|
|
22
22
|
return path.join(process.cwd(), "src", "utils", "translations");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Reads and parses a YAML config file (relative to the process working directory) and RETURNS its content, so that
|
|
27
|
+
* it can be passed straight to a consumer:
|
|
28
|
+
*
|
|
29
|
+
* new WebServer({ corsOriginOptions: yamlParsing.readFile(environment.corsOptions), ... })
|
|
30
|
+
*
|
|
31
|
+
* It used to parse the file and throw the result away (it returned `Promise<void>`), so such a call handed the
|
|
32
|
+
* consumer a Promise, and CORS silently ran with its defaults: any origin, no credentials.
|
|
33
|
+
*
|
|
34
|
+
* It is synchronous on purpose: a configuration is needed before the server starts. An unreadable or malformed
|
|
35
|
+
* file is logged and yields `{}`; callers that cannot run without a value must check for it.
|
|
36
|
+
*/
|
|
37
|
+
public readFile(filePath: string): Record<string, any> {
|
|
26
38
|
try {
|
|
27
|
-
|
|
28
|
-
await this.parsing(yamlContent);
|
|
39
|
+
return this.parsing(readFileSync(filePath, "utf-8"));
|
|
29
40
|
} catch (error: any) {
|
|
30
41
|
this.logger.error({
|
|
31
42
|
message: error.message,
|
|
@@ -34,15 +45,53 @@ export class YamlParsing {
|
|
|
34
45
|
stackTrace: error.stack,
|
|
35
46
|
httpCodeValue: HttpStatusCode.NOT_ACCEPTABLE
|
|
36
47
|
});
|
|
48
|
+
return {};
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
|
-
|
|
51
|
+
|
|
40
52
|
/**
|
|
53
|
+
* A YAML scalar as a typed value: `true` / `false` become booleans, `null` / `~` / empty become null,
|
|
54
|
+
* numbers become numbers, quoted text becomes the text inside the quotes, anything else the trimmed string.
|
|
55
|
+
*
|
|
56
|
+
* @param raw
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
private scalar(raw: string | undefined): any {
|
|
60
|
+
const value: string = (raw ?? "").trim();
|
|
61
|
+
if (value === "" || value === "~" || value === "null") return null;
|
|
62
|
+
if (value === "true") return true;
|
|
63
|
+
if (value === "false") return false;
|
|
64
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
|
|
65
|
+
if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
|
|
66
|
+
return value.slice(1, -1);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The value after `key:` — a scalar, or an inline list (`[a, b]`, `[]`).
|
|
73
|
+
*
|
|
74
|
+
* @param raw
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
private value(raw: string | undefined): any {
|
|
78
|
+
const value: string = (raw ?? "").replace(/\s+#.*$/, "").trim();
|
|
79
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
80
|
+
const inner: string = value.slice(1, -1).trim();
|
|
81
|
+
return inner === "" ? [] : inner.split(",").map((item: string): any => this.scalar(item));
|
|
82
|
+
}
|
|
83
|
+
return this.scalar(value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parses the supported YAML subset: `key: value` pairs (typed scalars), inline lists (`[a, b]`), block lists
|
|
88
|
+
* (`- item` lines under a key) and one level of nested `key: value` pairs. Full-line and trailing comments,
|
|
89
|
+
* and blank lines, are ignored.
|
|
41
90
|
*
|
|
42
91
|
* @param content
|
|
43
92
|
* @private
|
|
44
93
|
*/
|
|
45
|
-
private
|
|
94
|
+
private parsing(content: string): Record<any, any> {
|
|
46
95
|
const result: Record<string, any> = {};
|
|
47
96
|
const lines: string[] = content.split("\n");
|
|
48
97
|
let currentKey: string | null = null;
|
|
@@ -53,16 +102,27 @@ export class YamlParsing {
|
|
|
53
102
|
continue;
|
|
54
103
|
}
|
|
55
104
|
|
|
105
|
+
const listItemMatch: RegExpMatchArray | null = line.match(/^\s*-\s+(.*)$/);
|
|
106
|
+
if (listItemMatch && currentKey) {
|
|
107
|
+
if (!Array.isArray(result[currentKey])) {
|
|
108
|
+
result[currentKey] = [];
|
|
109
|
+
}
|
|
110
|
+
result[currentKey].push(this.scalar(listItemMatch[1].replace(/\s+#.*$/, "")));
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
56
114
|
const keyValueMatch: RegExpMatchArray | null = line.match(/^(\s*)([a-zA-Z0-9_]+):(?:\s*(.*))?$/);
|
|
57
115
|
if (keyValueMatch) {
|
|
58
116
|
const [, indent, key, value] = keyValueMatch;
|
|
59
117
|
// Handle nested objects (simple one-level nesting)
|
|
60
118
|
if (indent.length > 0 && currentKey) {
|
|
61
|
-
result[currentKey]
|
|
62
|
-
|
|
119
|
+
if (result[currentKey] === null || typeof result[currentKey] !== "object" || Array.isArray(result[currentKey])) {
|
|
120
|
+
result[currentKey] = {};
|
|
121
|
+
}
|
|
122
|
+
result[currentKey][key] = this.value(value);
|
|
63
123
|
} else {
|
|
64
124
|
currentKey = key;
|
|
65
|
-
result[key] = value
|
|
125
|
+
result[key] = this.value(value);
|
|
66
126
|
}
|
|
67
127
|
|
|
68
128
|
} else {
|
|
@@ -78,4 +138,4 @@ export class YamlParsing {
|
|
|
78
138
|
|
|
79
139
|
return result;
|
|
80
140
|
}
|
|
81
|
-
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Regression test for YamlParsing.readFile — run with `npm run build && npm test`.
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const entry = process.env.WEBAPP_CORE_DIST ?? path.join(packageRoot, "dist", "index.js");
|
|
12
|
+
|
|
13
|
+
// opticore-logger fixes ./logs/app.log at import time and expects it to exist: run in a scratch directory.
|
|
14
|
+
const run = (yaml) => {
|
|
15
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "yaml-parsing-"));
|
|
16
|
+
fs.mkdirSync(path.join(scratch, "logs"));
|
|
17
|
+
fs.writeFileSync(path.join(scratch, "logs", "app.log"), "");
|
|
18
|
+
fs.writeFileSync(path.join(scratch, ".env"), "DEFAULT_LOCAL_LANG=en\n");
|
|
19
|
+
if (yaml !== null) fs.writeFileSync(path.join(scratch, "config.yaml"), yaml);
|
|
20
|
+
|
|
21
|
+
const scenario = `
|
|
22
|
+
const { YamlParsing } = require(${JSON.stringify(entry)});
|
|
23
|
+
const log = console.log, error = console.error;
|
|
24
|
+
console.log = console.error = () => {};
|
|
25
|
+
const value = new YamlParsing("en", ${JSON.stringify(path.join(scratch, ".env"))}).readFile("config.yaml");
|
|
26
|
+
console.log = log; console.error = error;
|
|
27
|
+
process.stdout.write("RESULT:" + JSON.stringify({ isPromise: value instanceof Promise, value }));
|
|
28
|
+
`;
|
|
29
|
+
const output = execFileSync(process.execPath, ["-e", scenario], { cwd: scratch, encoding: "utf8" });
|
|
30
|
+
return JSON.parse(output.slice(output.indexOf("RESULT:") + 7));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
test("readFile RETURNS the parsed content (it used to return a Promise<void>)", () => {
|
|
34
|
+
const { isPromise, value } = run("a: 1\n");
|
|
35
|
+
assert.equal(isPromise, false);
|
|
36
|
+
assert.deepEqual(value, { a: 1 });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("scalars are typed: booleans, numbers, null, quoted text ('false' is no longer a truthy string)", () => {
|
|
40
|
+
const { value } = run([
|
|
41
|
+
"credentials: true", "flag: false", "maxAge: 600", "ratio: 1.5", "empty:", "nothing: null", "tilde: ~",
|
|
42
|
+
"text: hello world", "quoted: \"true\"", "single: 'x y'", "duration: 1d", "",
|
|
43
|
+
].join("\n"));
|
|
44
|
+
assert.deepEqual(value, { credentials: true, flag: false, maxAge: 600, ratio: 1.5, empty: null, nothing: null, tilde: null, text: "hello world", quoted: "true", single: "x y", duration: "1d" });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("inline lists and block lists are supported", () => {
|
|
48
|
+
const { value } = run([
|
|
49
|
+
"methods: [GET, POST, OPTIONS]", "none: []",
|
|
50
|
+
"origin:", " - https://app.example.com", " - http://localhost:3000 # dev", "",
|
|
51
|
+
"allowedHeaders:", "- Content-Type", "- X-Requested-With", "",
|
|
52
|
+
].join("\n"));
|
|
53
|
+
assert.deepEqual(value, {
|
|
54
|
+
methods: ["GET", "POST", "OPTIONS"], none: [],
|
|
55
|
+
origin: ["https://app.example.com", "http://localhost:3000"],
|
|
56
|
+
allowedHeaders: ["Content-Type", "X-Requested-With"],
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("comments, blank lines and one level of nesting still work", () => {
|
|
61
|
+
const { value } = run(["# a comment", "", "server:", " host: localhost", " port: 4201", "name: app # trailing", ""].join("\n"));
|
|
62
|
+
assert.deepEqual(value, { server: { host: "localhost", port: 4201 }, name: "app" });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a missing file is reported and yields {} instead of undefined", () => {
|
|
66
|
+
const result = run(null);
|
|
67
|
+
assert.equal(result.isPromise, false);
|
|
68
|
+
assert.deepEqual(result.value, {});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("the CORS use case: the result can be handed straight to cors()", () => {
|
|
72
|
+
const { value } = run(["origin:", " - https://app.example.com", "methods: [GET, POST]", "credentials: true", "maxAge: 600", ""].join("\n"));
|
|
73
|
+
assert.equal(value.credentials, true);
|
|
74
|
+
assert.deepEqual(value.origin, ["https://app.example.com"]);
|
|
75
|
+
assert.equal(typeof value.maxAge, "number");
|
|
76
|
+
});
|