opticore-webapp-core 1.0.22 → 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 CHANGED
@@ -546,7 +546,7 @@ var CLogLevel = {
546
546
  };
547
547
 
548
548
  // src/utils/parsing/parsingYaml.utils.ts
549
- var import_promises = require("fs/promises");
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
- async readFile(filePath) {
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
- const yamlContent = await (0, import_promises.readFile)(filePath, "utf-8");
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
- async parsing(content) {
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] = result[currentKey] || {};
621
- result[currentKey][key] = value?.trim() || null;
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?.trim() || null;
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 import_fs = __toESM(require("fs"), 1);
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 (import_fs.default.existsSync(fullPath)) {
737
- const env = import_fs.default.readFileSync(fullPath, "utf-8");
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 { readFile } from "fs/promises";
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
- async readFile(filePath) {
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
- const yamlContent = await readFile(filePath, "utf-8");
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
- async parsing(content) {
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] = result[currentKey] || {};
576
- result[currentKey][key] = value?.trim() || null;
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?.trim() || null;
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.22",
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": "echo \"Error: no test specified\" && exit 1",
10
+ "test": "node --test test/*.test.mjs",
11
11
  "build": "tsup"
12
12
  },
13
13
  "repository": {
@@ -30,11 +30,11 @@
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.30",
34
- "opticore-env-access": "^1.0.25",
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.11",
37
- "opticore-logger": "^1.0.33",
36
+ "opticore-http-response": "^1.0.12",
37
+ "opticore-logger": "^1.0.35",
38
38
  "opticore-translator": "^1.0.17",
39
39
  "pg": "^8.15.6"
40
40
  },
@@ -1,4 +1,4 @@
1
- import { readFile } from "fs/promises";
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
- public async readFile(filePath: string): Promise<void> {
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
- const yamlContent: string = await readFile(filePath, "utf-8");
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 async parsing(content: string): Promise<Record<any, any>> {
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] = result[currentKey] || {};
62
- result[currentKey][key] = value?.trim() || null;
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?.trim() || null;
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
+ });