@sentenel/node-utils 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.
- package/dist/cjs/index.cjs +741 -0
- package/dist/esm/index.js +688 -0
- package/package.json +52 -0
- package/src/dotenv/dotenv.async.ts +32 -0
- package/src/dotenv/dotenv.sync.ts +28 -0
- package/src/dotenv/index.ts +2 -0
- package/src/index.ts +2 -0
- package/src/paths/index.ts +1 -0
- package/src/paths/paths.test.ts +107 -0
- package/src/paths/paths.ts +33 -0
- package/src/paths/root.path.global.ts +124 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sentenel/node-utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reusable Node utilities for Sentenel (paths, dotenv helpers)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "aarya11 <arpitarya@users.noreply.github.com>",
|
|
7
|
+
"private": false,
|
|
8
|
+
"type": "module",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"main": "./dist/cjs/index.js",
|
|
17
|
+
"module": "./dist/esm/index.js",
|
|
18
|
+
"types": "./dist/types/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./dist/esm/index.js",
|
|
22
|
+
"require": "./dist/cjs/index.js",
|
|
23
|
+
"types": "./dist/types/index.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"sentenel",
|
|
31
|
+
"node",
|
|
32
|
+
"utils",
|
|
33
|
+
"paths",
|
|
34
|
+
"dotenv"
|
|
35
|
+
],
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@biomejs/biome": "2.3.14",
|
|
38
|
+
"@rstest/core": "0.8.3",
|
|
39
|
+
"@types/node": "25.2.1",
|
|
40
|
+
"chalk": "5.6.2",
|
|
41
|
+
"typescript": "5.9.3"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"dotenv": "17.2.4",
|
|
45
|
+
"dotenv-expand": "12.0.3"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "rslib build",
|
|
49
|
+
"dev": "rslib dev",
|
|
50
|
+
"test": "rstest"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const { NODE_ENV } = process.env;
|
|
4
|
+
|
|
5
|
+
async function asyncExpandDotEnv(path: string): Promise<void> {
|
|
6
|
+
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
|
|
7
|
+
const dotenvFiles = [
|
|
8
|
+
`${path}.${NODE_ENV}.local`,
|
|
9
|
+
`${path}.${NODE_ENV}`,
|
|
10
|
+
// Don't include `.env.local` for `test` environment to avoid
|
|
11
|
+
// polluting test environments with local development settings
|
|
12
|
+
NODE_ENV !== "test" ? `${path}.local` : null,
|
|
13
|
+
path,
|
|
14
|
+
].filter(Boolean) as string[];
|
|
15
|
+
|
|
16
|
+
// Load environment variables from .env* files in order
|
|
17
|
+
// https://github.com/motdotla/dotenv
|
|
18
|
+
// https://github.com/motedotla/dotenv-expand
|
|
19
|
+
await Promise.all(
|
|
20
|
+
dotenvFiles.map(async (dotenvFile) => {
|
|
21
|
+
if (existsSync(dotenvFile)) {
|
|
22
|
+
import("dotenv-expand").then(async () => {
|
|
23
|
+
import("dotenv").then((config) => {
|
|
24
|
+
config.config({ path: dotenvFile });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { asyncExpandDotEnv };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { config } from "dotenv";
|
|
3
|
+
import { expand } from "dotenv-expand";
|
|
4
|
+
|
|
5
|
+
const { NODE_ENV } = process.env;
|
|
6
|
+
|
|
7
|
+
async function syncExpandDotEnv(path: string): Promise<void> {
|
|
8
|
+
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
|
|
9
|
+
const dotenvFiles = [
|
|
10
|
+
`${path}.${NODE_ENV}.local`,
|
|
11
|
+
`${path}.${NODE_ENV}`,
|
|
12
|
+
// Don't include `.env.local` for `test` environment to avoid
|
|
13
|
+
// polluting test environments with local development settings
|
|
14
|
+
NODE_ENV !== "test" ? `${path}.local` : null,
|
|
15
|
+
path,
|
|
16
|
+
].filter(Boolean) as string[];
|
|
17
|
+
|
|
18
|
+
// Load environment variables from .env* files in order
|
|
19
|
+
// https://github.com/motdotla/dotenv
|
|
20
|
+
// https://github.com/motedotla/dotenv-expand
|
|
21
|
+
dotenvFiles.map(async (dotenvFile) => {
|
|
22
|
+
if (existsSync(dotenvFile)) {
|
|
23
|
+
expand(config({ path: dotenvFile }));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { syncExpandDotEnv };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./paths";
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "@rstest/core";
|
|
2
|
+
import { resolveRootPath, resolveWorkspaceDirectory } from "./paths";
|
|
3
|
+
|
|
4
|
+
describe("Paths Utilities", () => {
|
|
5
|
+
const originalEnv = process.env;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
process.env = { ...originalEnv };
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
process.env = originalEnv;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("resolveRootPath", () => {
|
|
16
|
+
it("should resolve a relative path against the root working directory", () => {
|
|
17
|
+
process.env.ROOT_WORKING_DIRECTORY = "/home/user/projects/sentenel";
|
|
18
|
+
const result = resolveRootPath("src/index.ts");
|
|
19
|
+
expect(result).toContain("src/index.ts");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should handle absolute paths correctly", () => {
|
|
23
|
+
process.env.ROOT_WORKING_DIRECTORY = "/home/user/projects/sentenel";
|
|
24
|
+
const result = resolveRootPath("packages/node-utils");
|
|
25
|
+
expect(result).toContain("packages/node-utils");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should throw error when root working directory is not defined", () => {
|
|
29
|
+
// Note: rootWorkingDirectory falls back to currentWorkingDirectory,
|
|
30
|
+
// so it will never be undefined. This test documents that behavior.
|
|
31
|
+
process.env.ROOT_WORKING_DIRECTORY = "/home/user/projects/sentenel";
|
|
32
|
+
const result = resolveRootPath("src/index.ts");
|
|
33
|
+
expect(result).toContain("src/index.ts");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should handle empty relative path", () => {
|
|
37
|
+
process.env.ROOT_WORKING_DIRECTORY = "/home/user/projects/sentenel";
|
|
38
|
+
const result = resolveRootPath("");
|
|
39
|
+
expect(result).toContain("sentenel");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should handle nested relative paths", () => {
|
|
43
|
+
process.env.ROOT_WORKING_DIRECTORY = "/home/user/projects/sentenel";
|
|
44
|
+
const result = resolveRootPath("packages/node-utils/src/index.ts");
|
|
45
|
+
expect(result).toContain("packages/node-utils/src/index.ts");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should handle paths with .. navigation", () => {
|
|
49
|
+
process.env.ROOT_WORKING_DIRECTORY = "/home/user/projects/sentenel";
|
|
50
|
+
const result = resolveRootPath("packages/../src");
|
|
51
|
+
expect(result).toContain("src");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("resolveWorkspaceDirectory", () => {
|
|
56
|
+
it("should resolve a relative path against the current working directory", () => {
|
|
57
|
+
const result = resolveWorkspaceDirectory("src/index.ts");
|
|
58
|
+
expect(result).toContain("src/index.ts");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should handle absolute paths correctly", () => {
|
|
62
|
+
const result = resolveWorkspaceDirectory("packages/node-utils");
|
|
63
|
+
expect(result).toContain("packages/node-utils");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should throw error when current working directory is not defined", () => {
|
|
67
|
+
expect(() => {
|
|
68
|
+
resolveWorkspaceDirectory("src/index.ts");
|
|
69
|
+
}).not.toThrow();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should handle empty relative path", () => {
|
|
73
|
+
const result = resolveWorkspaceDirectory("");
|
|
74
|
+
expect(result).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should handle nested relative paths", () => {
|
|
78
|
+
const result = resolveWorkspaceDirectory("packages/node-utils/src/index.ts");
|
|
79
|
+
expect(result).toContain("packages/node-utils/src/index.ts");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should handle paths with .. navigation", () => {
|
|
83
|
+
const result = resolveWorkspaceDirectory("packages/../src");
|
|
84
|
+
expect(result).toContain("src");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("Path resolution integration", () => {
|
|
89
|
+
it("should resolve different paths consistently when environment is set", () => {
|
|
90
|
+
process.env.ROOT_WORKING_DIRECTORY = "/home/user/projects/sentenel";
|
|
91
|
+
|
|
92
|
+
const rootPath1 = resolveRootPath("src/utils");
|
|
93
|
+
const rootPath2 = resolveRootPath("src/utils");
|
|
94
|
+
|
|
95
|
+
expect(rootPath1).toBe(rootPath2);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should normalize paths correctly", () => {
|
|
99
|
+
process.env.ROOT_WORKING_DIRECTORY = "/home/user/projects/sentenel";
|
|
100
|
+
|
|
101
|
+
const result1 = resolveRootPath("./src/index.ts");
|
|
102
|
+
const result2 = resolveRootPath("src/index.ts");
|
|
103
|
+
|
|
104
|
+
expect(result1).toBe(result2);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { ProcessEnvProp } from "./root.path.global";
|
|
4
|
+
|
|
5
|
+
const currentWorkingDirectory: string = realpathSync(process.cwd());
|
|
6
|
+
const rootWorkingDirectory: string = realpathSync(
|
|
7
|
+
process.env[ProcessEnvProp.RootWorkingDirectory] || currentWorkingDirectory,
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
function resolveRootPath(relativePath: string): string {
|
|
11
|
+
if (!rootWorkingDirectory) {
|
|
12
|
+
console.log(
|
|
13
|
+
`Define "process.env[ProcessEnvProp.WorkingDirLogsLevel]"='none' | 'info' | 'verbose' to enable logging of the working directory resolution process.`,
|
|
14
|
+
);
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Root working directory is not defined. Please set the environment variable ${ProcessEnvProp.RootWorkingDirectory} to the desired root working directory path.`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return path.resolve(rootWorkingDirectory, relativePath);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveWorkspaceDirectory(relativePath: string): string {
|
|
24
|
+
if (!currentWorkingDirectory) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Current working directory is not defined. Please set the environment variable ${ProcessEnvProp.RootWorkingDirectory} to the desired root working directory path.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return path.resolve(currentWorkingDirectory, relativePath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { resolveRootPath, resolveWorkspaceDirectory };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import Module from "node:module";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
|
|
5
|
+
enum ProcessEnvProp {
|
|
6
|
+
WorkingDirLogsLevel = "ENABLE_WORKING_DIR_LOGS_LEVEL",
|
|
7
|
+
RootWorkingDirectory = "ROOT_WORKING_DIRECTORY",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum WorkingDirLogsLevel {
|
|
11
|
+
None = "none",
|
|
12
|
+
Info = "info",
|
|
13
|
+
Verbose = "verbose",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function logInfo(...args: unknown[]): void {
|
|
17
|
+
console.info("SENTENEL PATH::", chalk.blueBright(...args.map((arg) => String(arg))));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getRootWorkingDirectory(): string | undefined {
|
|
21
|
+
const workingDirLogsLevel: WorkingDirLogsLevel =
|
|
22
|
+
(process.env[ProcessEnvProp.WorkingDirLogsLevel] as WorkingDirLogsLevel) || WorkingDirLogsLevel.None;
|
|
23
|
+
if (!Object.values(WorkingDirLogsLevel).includes(workingDirLogsLevel)) {
|
|
24
|
+
console.error(
|
|
25
|
+
`Invalid value for environment variable ${ProcessEnvProp.WorkingDirLogsLevel}: ${workingDirLogsLevel}. Defaulting to ${WorkingDirLogsLevel.None}.`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (workingDirLogsLevel === WorkingDirLogsLevel.Info || workingDirLogsLevel === WorkingDirLogsLevel.Verbose) {
|
|
29
|
+
logInfo(`Getting root working directory from environment variable ${ProcessEnvProp.RootWorkingDirectory}...`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isVerbose = workingDirLogsLevel === WorkingDirLogsLevel.Verbose;
|
|
33
|
+
const isInfo = workingDirLogsLevel === WorkingDirLogsLevel.Info || isVerbose;
|
|
34
|
+
|
|
35
|
+
const currentWorkingDirectory: string = realpathSync(process.cwd());
|
|
36
|
+
|
|
37
|
+
const nodeModulesPathList: string[] = (Module as any)._nodeModulePaths(currentWorkingDirectory);
|
|
38
|
+
|
|
39
|
+
isVerbose && logInfo("Current working directory:", currentWorkingDirectory);
|
|
40
|
+
isVerbose && logInfo("Node modules path list:", nodeModulesPathList);
|
|
41
|
+
|
|
42
|
+
const packageJsonPathList: string[] = nodeModulesPathList.map((nodeModulesPath: string) => {
|
|
43
|
+
const packageJsonPath = nodeModulesPath.replace("node_modules", "package.json");
|
|
44
|
+
return packageJsonPath;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
isVerbose && logInfo("Package.json path list:", packageJsonPathList);
|
|
48
|
+
|
|
49
|
+
const packageJsonExistsPathList: string[] = packageJsonPathList.filter((packageJsonPath: string) => {
|
|
50
|
+
try {
|
|
51
|
+
realpathSync(packageJsonPath);
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
isVerbose && logInfo("Existing package.json path list:", packageJsonExistsPathList);
|
|
59
|
+
|
|
60
|
+
if (packageJsonExistsPathList.length === 0) {
|
|
61
|
+
isInfo && logInfo("No package.json found in node modules paths.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const workspacePackageJsonList: string[] = packageJsonExistsPathList.filter((packageJsonPath: string) => {
|
|
65
|
+
const packageJsonBuffer = readFileSync(packageJsonPath);
|
|
66
|
+
|
|
67
|
+
//@ts-expect-error
|
|
68
|
+
const packageJsonContent = JSON.parse(packageJsonBuffer);
|
|
69
|
+
|
|
70
|
+
if (packageJsonContent?.workspaces && packageJsonContent?.workspaces.length > 0) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
isVerbose && logInfo("Workspace package.json list:", workspacePackageJsonList);
|
|
77
|
+
|
|
78
|
+
if (workspacePackageJsonList.length === 0) {
|
|
79
|
+
isInfo && logInfo("No workspace package.json found in node modules paths.");
|
|
80
|
+
|
|
81
|
+
const rootWorkingDirectory = currentWorkingDirectory;
|
|
82
|
+
if (!rootWorkingDirectory) {
|
|
83
|
+
throw new Error(`Environment variable ${ProcessEnvProp.RootWorkingDirectory} is not set.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
isInfo && logInfo(`Current working directory determined: ${currentWorkingDirectory}`);
|
|
87
|
+
isInfo && logInfo(`Root working directory determined: ${rootWorkingDirectory}`);
|
|
88
|
+
process.env[ProcessEnvProp.RootWorkingDirectory] = rootWorkingDirectory;
|
|
89
|
+
|
|
90
|
+
return rootWorkingDirectory;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (workspacePackageJsonList.length === 1) {
|
|
94
|
+
isInfo && logInfo(`Single workspace package.json file found: ${workspacePackageJsonList[0]}`);
|
|
95
|
+
|
|
96
|
+
const rootPackageJsonPath: string = realpathSync(workspacePackageJsonList[0]);
|
|
97
|
+
const rootWorkingDirectory: string = rootPackageJsonPath.replace("/package.json", "");
|
|
98
|
+
|
|
99
|
+
isInfo && logInfo(`Current working directory determined: ${currentWorkingDirectory}`);
|
|
100
|
+
isInfo && logInfo(`Root working directory determined: ${rootWorkingDirectory}`);
|
|
101
|
+
process.env[ProcessEnvProp.RootWorkingDirectory] = rootWorkingDirectory;
|
|
102
|
+
|
|
103
|
+
return rootWorkingDirectory;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (workspacePackageJsonList.length > 1) {
|
|
107
|
+
isInfo && logInfo(`Multiple workspace package.json files found: ${workspacePackageJsonList.join(", ")}`);
|
|
108
|
+
|
|
109
|
+
// Fallback (should not reach here)
|
|
110
|
+
const rootWorkingDirectory = currentWorkingDirectory;
|
|
111
|
+
|
|
112
|
+
isInfo && logInfo(`Current working directory determined (fallback): ${currentWorkingDirectory}`);
|
|
113
|
+
isInfo && logInfo(`Root working directory determined (fallback): ${rootWorkingDirectory}`);
|
|
114
|
+
process.env[ProcessEnvProp.RootWorkingDirectory] = rootWorkingDirectory;
|
|
115
|
+
|
|
116
|
+
return rootWorkingDirectory;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.error("Unable to determine root working directory.");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getRootWorkingDirectory();
|
|
123
|
+
|
|
124
|
+
export { ProcessEnvProp, WorkingDirLogsLevel, getRootWorkingDirectory };
|