@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.
- package/dist/config/config-loader.js +107 -29
- package/dist/file-exists.js +34 -0
- package/dist/getRootPackageDirname.js +5 -0
- package/dist/index.js +4 -1
- package/dist/types/config/config-loader.d.ts +32 -4
- package/dist/types/file-exists.d.ts +12 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +2 -1
|
@@ -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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
process.exit(1);
|
|
73
|
+
return [null, new Error(`Failed to read config file at path: ${path}`)];
|
|
31
74
|
}
|
|
32
75
|
try {
|
|
33
|
-
|
|
76
|
+
parsedYml = (0, yaml_1.parse)(data, { prettyErrors: true });
|
|
34
77
|
}
|
|
35
78
|
catch (e) {
|
|
36
|
-
|
|
37
|
-
process.exit(1);
|
|
79
|
+
return [null, new Error(`Failed to parse YAML file at path: ${path}`)];
|
|
38
80
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
package/dist/types/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsnw/srv-utils",
|
|
3
|
-
"version": "1.0.
|
|
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"
|