@jsnw/srv-utils 1.0.3 → 1.0.5

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.
@@ -5,57 +5,135 @@ const node_path_1 = require("node:path");
5
5
  const node_fs_1 = require("node:fs");
6
6
  const zod_1 = require("zod");
7
7
  const yaml_1 = require("yaml");
8
+ const file_exists_1 = require("../file-exists");
8
9
  const getRootPackageDirname_1 = require("../getRootPackageDirname");
9
10
  const PKG_ROOT_REGEX = /^%pkgroot[\/\\]/i;
10
11
  class ConfigLoader {
12
+ static _instance;
13
+ /**
14
+ * @returns {ConfigLoader}
15
+ */
16
+ static instance() {
17
+ if (!ConfigLoader._instance)
18
+ ConfigLoader._instance = new ConfigLoader();
19
+ return ConfigLoader._instance;
20
+ }
11
21
  /**
12
22
  * @template {z.ZodObject} S
13
23
  * @template {object} P
14
24
  * @param {string} path You can use %pkgroot prefix for automatic project root resolution by AppConfigLoader.
15
25
  * Example: %pkgroot/config.yml
16
26
  * @param {S} schema
17
- * @param {P} addProps
18
- * @returns {ResolvedConfig<S>}
27
+ * @param {P} [addProps]
28
+ * @returns {ResolvedConfig<S, P>}
19
29
  */
20
- static loadAndValidate(path, schema, addProps) {
21
- if (PKG_ROOT_REGEX.test(path))
22
- path = path.replace(PKG_ROOT_REGEX, (0, getRootPackageDirname_1.getRootPackageDirnameSync)() + node_path_1.sep);
23
- let data = undefined;
24
- let parsedYaml = undefined;
30
+ static loadConfig(path, schema, addProps) {
31
+ const loader = ConfigLoader.instance();
32
+ const [yaml, loadError] = loader.loadYamlFile(path);
33
+ if (loadError) {
34
+ console.error(loadError.message);
35
+ process.exit(1);
36
+ }
37
+ const [processedYaml, includeErrors] = loader.processIncludes(yaml);
38
+ if (includeErrors && includeErrors.length > 0) {
39
+ for (const err of includeErrors)
40
+ console.error(`$include error: ${err.message}`);
41
+ process.exit(1);
42
+ }
43
+ const [validatedYaml, validateError] = loader.validateYaml(processedYaml, schema);
44
+ if (validateError) {
45
+ console.error(validateError.message);
46
+ process.exit(1);
47
+ }
48
+ //@ts-expect-error
49
+ validatedYaml['isDev'] = (process?.env?.APP_CONTEXT
50
+ ?? process?.env?.APPLICATION_CONTEXT
51
+ ?? process?.env?.NODE_ENV
52
+ ?? '').toLowerCase() !== 'production';
53
+ return {
54
+ ...validatedYaml,
55
+ ...(addProps ?? {})
56
+ };
57
+ }
58
+ constructor() { }
59
+ /**
60
+ * @param {string} path
61
+ * @returns {ErrorResult<any, Error>}
62
+ * @protected
63
+ */
64
+ loadYamlFile(path) {
65
+ path = this.resolvePkgRootPath(path);
66
+ if (!(0, file_exists_1.fileExistsSync)(path))
67
+ return [null, new Error(`YAML file does not exists at path: ${path}`)];
68
+ let data = undefined, parsedYml = undefined;
25
69
  try {
26
70
  data = (0, node_fs_1.readFileSync)(path, 'utf-8');
27
71
  }
28
72
  catch (e) {
29
- console.error(`Failed to read config file at path: ${path}\nError: ${e?.message ?? '-'}; syscall: ${e?.syscall ?? '-'}`);
30
- process.exit(1);
73
+ return [null, new Error(`Failed to read config file at path: ${path}`)];
31
74
  }
32
75
  try {
33
- parsedYaml = (0, yaml_1.parse)(data, { prettyErrors: true });
76
+ parsedYml = (0, yaml_1.parse)(data, { prettyErrors: true });
34
77
  }
35
78
  catch (e) {
36
- console.error(`Failed to parse YAML file at path: ${path}\nError: ${e?.message ?? '-'}`);
37
- process.exit(1);
79
+ return [null, new Error(`Failed to parse YAML file at path: ${path}`)];
38
80
  }
39
- const extendedSchema = schema.transform(v => ({
40
- isDev: (process?.env?.APP_CONTEXT
41
- ?? process?.env?.APPLICATION_CONTEXT
42
- ?? process?.env?.NODE_ENV
43
- ?? '').toLowerCase() !== 'production',
44
- ...v
45
- }));
46
- const { data: config, error, success } = extendedSchema.safeParse(parsedYaml);
81
+ return [parsedYml, null];
82
+ }
83
+ /**
84
+ * @template {z.ZodTypeAny} T
85
+ * @param data
86
+ * @param {T} schema
87
+ * @returns {ErrorResult<output<T>, Error>}
88
+ * @protected
89
+ */
90
+ validateYaml(data, schema) {
91
+ const { data: parsed, error, success } = schema.safeParse(data);
47
92
  if (!success) {
48
- if (error && error instanceof zod_1.ZodError) {
49
- console.error(`Failed to validate config file at path ${path}. Error: ${zod_1.z.prettifyError(error)}`);
50
- process.exit(1);
93
+ if (error && error instanceof zod_1.ZodError)
94
+ return [null, new Error(`Failed to validate yaml (#1)`)];
95
+ return [null, new Error(`Failed to validate yaml (#2)`)];
96
+ }
97
+ return [parsed, null];
98
+ }
99
+ /**
100
+ * @param yaml
101
+ * @returns {ErrorResult<any, Error[]>}
102
+ * @protected
103
+ */
104
+ processIncludes(yaml) {
105
+ if (typeof yaml !== 'object')
106
+ return yaml;
107
+ const nodesToVisit = [yaml], errors = [];
108
+ for (let i = 0; i < nodesToVisit.length; i++) {
109
+ const node = nodesToVisit[i];
110
+ if (typeof node !== 'object' || Array.isArray(node))
111
+ continue;
112
+ if (node['$include'] && typeof node['$include'] === 'string') {
113
+ const [loadedYaml, loadError] = this.loadYamlFile(node['$include']);
114
+ delete node['$include'];
115
+ if (loadError) {
116
+ errors.push(loadError);
117
+ }
118
+ else {
119
+ for (const [k, v] of Object.entries(loadedYaml))
120
+ node[k] = v;
121
+ }
122
+ }
123
+ for (const k of Object.keys(node)) {
124
+ if (Object.hasOwn(node, k)
125
+ && typeof node[k] === 'object'
126
+ && !Array.isArray(node[k]))
127
+ nodesToVisit.push(node[k]);
51
128
  }
52
- console.error(`Failed to parse config file at path ${path}`);
53
- process.exit(1);
54
129
  }
55
- return {
56
- ...config,
57
- ...(addProps ? {} : addProps)
58
- };
130
+ return [yaml, errors];
131
+ }
132
+ //region Utils
133
+ resolvePkgRootPath(path) {
134
+ if (PKG_ROOT_REGEX.test(path))
135
+ path = path.replace(PKG_ROOT_REGEX, (0, getRootPackageDirname_1.getRootPackageDirnameSync)() + node_path_1.sep);
136
+ return path;
59
137
  }
60
138
  }
61
139
  exports.ConfigLoader = ConfigLoader;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fileExists = fileExists;
4
+ exports.fileExistsSync = fileExistsSync;
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_fs_1 = require("node:fs");
7
+ /**
8
+ * @param {string} path
9
+ * @param {number} [mode]
10
+ * @returns {boolean}
11
+ */
12
+ async function fileExists(path, mode = node_fs_1.constants.F_OK | node_fs_1.constants.R_OK) {
13
+ try {
14
+ await (0, promises_1.access)(path, mode);
15
+ return true;
16
+ }
17
+ catch (e) {
18
+ return false;
19
+ }
20
+ }
21
+ /**
22
+ * @param {string} path
23
+ * @param {number} [mode]
24
+ * @returns {boolean}
25
+ */
26
+ function fileExistsSync(path, mode = node_fs_1.constants.F_OK | node_fs_1.constants.R_OK) {
27
+ try {
28
+ (0, node_fs_1.accessSync)(path, mode);
29
+ return true;
30
+ }
31
+ catch (e) {
32
+ return false;
33
+ }
34
+ }
@@ -11,6 +11,11 @@ function getRootPackageDirnameSync() {
11
11
  if (cachedRootPackageDirname !== null)
12
12
  return cachedRootPackageDirname;
13
13
  let path = (0, node_path_1.resolve)(__dirname, '..'), lastValidPath = path;
14
+ if (/[\\\/]node_modules[\\\/]/i.test(path)) {
15
+ const pieces = path.split(/[\\\/]node_modules[\\\/]/i);
16
+ if (pieces.length > 0)
17
+ path = pieces[0];
18
+ }
14
19
  while (true) {
15
20
  const packageJsonPath = (0, node_path_1.resolve)(path, 'package.json');
16
21
  try {
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.withNest = exports.configSchemas = exports.ConfigLoader = exports.getRootPackageDirnameSync = exports.useTsconfigPaths = void 0;
3
+ exports.withNest = exports.configSchemas = exports.ConfigLoader = exports.getRootPackageDirnameSync = exports.useTsconfigPaths = exports.fileExistsSync = exports.fileExists = void 0;
4
+ var file_exists_1 = require("./file-exists");
5
+ Object.defineProperty(exports, "fileExists", { enumerable: true, get: function () { return file_exists_1.fileExists; } });
6
+ Object.defineProperty(exports, "fileExistsSync", { enumerable: true, get: function () { return file_exists_1.fileExistsSync; } });
4
7
  var useTsconfigPaths_1 = require("./useTsconfigPaths");
5
8
  Object.defineProperty(exports, "useTsconfigPaths", { enumerable: true, get: function () { return useTsconfigPaths_1.useTsconfigPaths; } });
6
9
  var getRootPackageDirname_1 = require("./getRootPackageDirname");
@@ -1,14 +1,42 @@
1
1
  import { z } from 'zod';
2
+ import { type ErrorResult } from '@jsnw/common-utils';
2
3
  import { ResolvedConfig } from './config-loader.types';
3
- export declare abstract class ConfigLoader {
4
+ export declare class ConfigLoader {
5
+ private static _instance;
6
+ /**
7
+ * @returns {ConfigLoader}
8
+ */
9
+ static instance(): ConfigLoader;
4
10
  /**
5
11
  * @template {z.ZodObject} S
6
12
  * @template {object} P
7
13
  * @param {string} path You can use %pkgroot prefix for automatic project root resolution by AppConfigLoader.
8
14
  * Example: %pkgroot/config.yml
9
15
  * @param {S} schema
10
- * @param {P} addProps
11
- * @returns {ResolvedConfig<S>}
16
+ * @param {P} [addProps]
17
+ * @returns {ResolvedConfig<S, P>}
18
+ */
19
+ static loadConfig<S extends z.ZodObject, P extends object | undefined = undefined>(path: string, schema: S, addProps?: P): ResolvedConfig<S, P>;
20
+ private constructor();
21
+ /**
22
+ * @param {string} path
23
+ * @returns {ErrorResult<any, Error>}
24
+ * @protected
25
+ */
26
+ protected loadYamlFile(path: string): ErrorResult<any, Error>;
27
+ /**
28
+ * @template {z.ZodTypeAny} T
29
+ * @param data
30
+ * @param {T} schema
31
+ * @returns {ErrorResult<output<T>, Error>}
32
+ * @protected
33
+ */
34
+ protected validateYaml<T extends z.ZodTypeAny>(data: any, schema: T): ErrorResult<z.infer<T>, Error>;
35
+ /**
36
+ * @param yaml
37
+ * @returns {ErrorResult<any, Error[]>}
38
+ * @protected
12
39
  */
13
- static loadAndValidate<S extends z.ZodObject, P extends object | undefined = undefined>(path: string, schema: S, addProps?: P): ResolvedConfig<S, P>;
40
+ protected processIncludes(yaml: any): ErrorResult<any, Error[]>;
41
+ private resolvePkgRootPath;
14
42
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @param {string} path
3
+ * @param {number} [mode]
4
+ * @returns {boolean}
5
+ */
6
+ export declare function fileExists(path: string, mode?: number): Promise<boolean>;
7
+ /**
8
+ * @param {string} path
9
+ * @param {number} [mode]
10
+ * @returns {boolean}
11
+ */
12
+ export declare function fileExistsSync(path: string, mode?: number): boolean;
@@ -1,3 +1,4 @@
1
+ export { fileExists, fileExistsSync } from './file-exists';
1
2
  export { useTsconfigPaths } from './useTsconfigPaths';
2
3
  export { getRootPackageDirnameSync } from './getRootPackageDirname';
3
4
  export { ConfigLoader, configSchemas, type ResolvedConfig } from './config';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsnw/srv-utils",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Server-side utilities for Node.js/TypeScript: tsconfig paths, Nest helpers, and config loading.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -40,6 +40,7 @@
40
40
  },
41
41
  "homepage": "https://github.com/pvbaliuk/jsn-srv-utils#readme",
42
42
  "dependencies": {
43
+ "@jsnw/common-utils": "^1.0.0",
43
44
  "ms": "^2.1.3",
44
45
  "yaml": "^2.8.2",
45
46
  "zod": "^4.3.6"