@safe-hand/safe-env-check 1.0.3 → 1.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/.github/workflows/release.yml +5 -3
- package/README.md +27 -10
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +170 -0
- package/package.json +7 -2
- package/src/cli.ts +131 -8
- package/tests/cli.test.ts +200 -0
- package/tsconfig.json +1 -0
|
@@ -3,7 +3,7 @@ name: Release
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
tags:
|
|
6
|
-
-
|
|
6
|
+
- "v*.*.*"
|
|
7
7
|
|
|
8
8
|
jobs:
|
|
9
9
|
release:
|
|
@@ -14,8 +14,8 @@ jobs:
|
|
|
14
14
|
- name: Set up Node.js
|
|
15
15
|
uses: actions/setup-node@v3
|
|
16
16
|
with:
|
|
17
|
-
node-version:
|
|
18
|
-
registry-url:
|
|
17
|
+
node-version: "20"
|
|
18
|
+
registry-url: "https://registry.npmjs.org"
|
|
19
19
|
|
|
20
20
|
- name: Install dependencies
|
|
21
21
|
run: npm install
|
|
@@ -30,5 +30,7 @@ jobs:
|
|
|
30
30
|
|
|
31
31
|
- name: Create GitHub Release
|
|
32
32
|
uses: ncipollo/release-action@v1
|
|
33
|
+
env:
|
|
34
|
+
GITHUB_TOKEN: ${{secrets.GH_PAT}}
|
|
33
35
|
with:
|
|
34
36
|
tag: ${{ github.ref_name }}
|
package/README.md
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|

|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
A tiny TypeScript library to validate environment variables using a schema with support for:
|
|
9
8
|
|
|
10
9
|
- ✅ Type validation
|
|
@@ -22,7 +21,9 @@ A tiny TypeScript library to validate environment variables using a schema with
|
|
|
22
21
|
```bash
|
|
23
22
|
npm install @safe-hand/safe-env-check
|
|
24
23
|
```
|
|
24
|
+
|
|
25
25
|
or
|
|
26
|
+
|
|
26
27
|
```bash
|
|
27
28
|
yarn add @safe-hand/safe-env-check
|
|
28
29
|
```
|
|
@@ -44,6 +45,7 @@ yarn add @safe-hand/safe-env-check
|
|
|
44
45
|
## Basic Usage
|
|
45
46
|
|
|
46
47
|
### Define a schema
|
|
48
|
+
|
|
47
49
|
```ts
|
|
48
50
|
const schema = {
|
|
49
51
|
PORT: { type: "number", required: true },
|
|
@@ -57,26 +59,29 @@ const schema = {
|
|
|
57
59
|
```
|
|
58
60
|
|
|
59
61
|
### Validate environment variables
|
|
62
|
+
|
|
60
63
|
```ts
|
|
61
64
|
import { validateEnv } from "@safe-hand/safe-env-check";
|
|
62
65
|
|
|
63
66
|
const env = validateEnv(schema);
|
|
64
67
|
|
|
65
|
-
console.log(env.PORT);
|
|
66
|
-
console.log(env.NODE_ENV);
|
|
68
|
+
console.log(env.PORT); // number
|
|
69
|
+
console.log(env.NODE_ENV); // "development" | "production"
|
|
67
70
|
```
|
|
68
71
|
|
|
69
72
|
## Schema Options
|
|
73
|
+
|
|
70
74
|
Each environment variables supports the following options:
|
|
71
75
|
|
|
72
|
-
| Field | Type
|
|
73
|
-
| ---------- |
|
|
74
|
-
| `type` | `"string" or "number" or "boolean" or "enum"` | Expected value type
|
|
75
|
-
| `required` | `boolean`
|
|
76
|
-
| `default` | `any`
|
|
77
|
-
| `values` | `string[]`
|
|
76
|
+
| Field | Type | Description |
|
|
77
|
+
| ---------- | ---------------------------------------------- | -------------------------------- |
|
|
78
|
+
| `type` | `"string" or "number" or "boolean" or "enum"` | Expected value type |
|
|
79
|
+
| `required` | `boolean` | Whether the variable is required |
|
|
80
|
+
| `default` | `any` | Default value if not provided |
|
|
81
|
+
| `values` | `string[]` | Required for `enum` type |
|
|
78
82
|
|
|
79
83
|
## Example
|
|
84
|
+
|
|
80
85
|
```ts
|
|
81
86
|
DATABASE_URL: { type: "string", required: true },
|
|
82
87
|
DEBUG: { type: "boolean", default: false },
|
|
@@ -84,14 +89,19 @@ MODE: { type: "enum", values: ["dev", "prod"] }
|
|
|
84
89
|
```
|
|
85
90
|
|
|
86
91
|
## Strict Mode
|
|
92
|
+
|
|
87
93
|
Disallow environment variables that are not defined in the schema.
|
|
94
|
+
|
|
88
95
|
```ts
|
|
89
96
|
validateEnv(schema, { strict: true });
|
|
90
97
|
```
|
|
98
|
+
|
|
91
99
|
If extra variables are found, validation will fail.
|
|
92
100
|
|
|
93
101
|
## Custom Error Formatter
|
|
102
|
+
|
|
94
103
|
You can control how errors are displayed:
|
|
104
|
+
|
|
95
105
|
```ts
|
|
96
106
|
validateEnv(schema, {
|
|
97
107
|
formatError: (errors) => `Config error:\n${errors.join("\n")}`,
|
|
@@ -103,6 +113,7 @@ validateEnv(schema, {
|
|
|
103
113
|
By default, the library loads .env automatically using dotenv.
|
|
104
114
|
|
|
105
115
|
Example .env file:
|
|
116
|
+
|
|
106
117
|
```bash
|
|
107
118
|
PORT=3000
|
|
108
119
|
JWT_SECRET=supersecret
|
|
@@ -112,6 +123,7 @@ NODE_ENV=development
|
|
|
112
123
|
## CLI Usage
|
|
113
124
|
|
|
114
125
|
Create a schema file called env.schema.js:
|
|
126
|
+
|
|
115
127
|
```ts
|
|
116
128
|
module.exports = {
|
|
117
129
|
PORT: { type: "number", required: true },
|
|
@@ -120,10 +132,15 @@ module.exports = {
|
|
|
120
132
|
```
|
|
121
133
|
|
|
122
134
|
Run validation:
|
|
135
|
+
|
|
123
136
|
```bash
|
|
124
137
|
npx safe-env-check env.schema.js
|
|
138
|
+
npx safe-env-check env.schema.js
|
|
139
|
+
npx safe-env-check --schema env.schema.js --strict
|
|
140
|
+
npx safe-env-check env.schema.js --env-file .env.production
|
|
141
|
+
npx safe-env-check env.schema.js --format json
|
|
125
142
|
```
|
|
126
143
|
|
|
127
144
|
## License
|
|
128
145
|
|
|
129
|
-
MIT © Shakhawat Hossain
|
|
146
|
+
MIT © Shakhawat Hossain
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// src/cli.ts
|
|
26
|
+
var import_dotenv = __toESM(require("dotenv"));
|
|
27
|
+
var import_path = __toESM(require("path"));
|
|
28
|
+
var import_fs = __toESM(require("fs"));
|
|
29
|
+
|
|
30
|
+
// package.json
|
|
31
|
+
var version = "1.1.0";
|
|
32
|
+
|
|
33
|
+
// src/validateEnv.ts
|
|
34
|
+
var validateEnv = (schema, options = {}) => {
|
|
35
|
+
const errors = [];
|
|
36
|
+
const result = {};
|
|
37
|
+
if (options.strict) {
|
|
38
|
+
const schemaKeys = Object.keys(schema);
|
|
39
|
+
const envKeys = Object.keys(process.env);
|
|
40
|
+
const unknownKeys = envKeys.filter((key) => !schemaKeys.includes(key));
|
|
41
|
+
if (unknownKeys.length)
|
|
42
|
+
errors.push(`Unknown evn variables: ${unknownKeys.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
for (const key in schema) {
|
|
45
|
+
const rule = schema[key];
|
|
46
|
+
const rawValue = process.env[key];
|
|
47
|
+
if (!rawValue) {
|
|
48
|
+
if (rule.required && rule.default === void 0) {
|
|
49
|
+
errors.push(`${key} is required`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
result[key] = rule.default;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
switch (rule.type) {
|
|
56
|
+
case "string":
|
|
57
|
+
result[key] = rawValue;
|
|
58
|
+
break;
|
|
59
|
+
case "number":
|
|
60
|
+
const num = Number(rawValue);
|
|
61
|
+
if (isNaN(num)) errors.push(`${key} must be a number`);
|
|
62
|
+
else result[key] = num;
|
|
63
|
+
break;
|
|
64
|
+
case "boolean":
|
|
65
|
+
result[key] = rawValue === "true";
|
|
66
|
+
break;
|
|
67
|
+
case "enum":
|
|
68
|
+
if (!rule.values.includes(rawValue)) {
|
|
69
|
+
errors.push(`${key} must be one of: ${rule.values.join(", ")}`);
|
|
70
|
+
} else {
|
|
71
|
+
result[key] = rawValue;
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (errors.length) {
|
|
77
|
+
const message = options.formatError ? options.formatError(errors) : defaultErrorFormatter(errors);
|
|
78
|
+
throw new Error(message);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
var defaultErrorFormatter = (errors) => {
|
|
83
|
+
return "\u274C Environment validation failed:\n" + errors.map((e) => `- ${e}`).join("\n");
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/cli.ts
|
|
87
|
+
var args = process.argv.slice(2);
|
|
88
|
+
var getArg = (flag) => {
|
|
89
|
+
const index = args.indexOf(flag);
|
|
90
|
+
return index !== -1 ? args[index + 1] : void 0;
|
|
91
|
+
};
|
|
92
|
+
var hasFlag = (flag) => args.includes(flag);
|
|
93
|
+
var getPositionalArgs = (args2) => {
|
|
94
|
+
return args2.filter((arg, index) => {
|
|
95
|
+
if (arg.startsWith("--")) return false;
|
|
96
|
+
const prev = args2[index - 1];
|
|
97
|
+
if (prev === "--schema" || prev === "--env-file" || prev === "--format") {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
var printHelp = () => {
|
|
104
|
+
console.log(`
|
|
105
|
+
safe-env-check
|
|
106
|
+
|
|
107
|
+
Usage:
|
|
108
|
+
safe-env-check <schema-file> [options]
|
|
109
|
+
|
|
110
|
+
Options:
|
|
111
|
+
--schema <file> Path to schema file
|
|
112
|
+
--strict Enable strict mode
|
|
113
|
+
--env-file <file> Load a custom .env file
|
|
114
|
+
--format json Output errors as JSON
|
|
115
|
+
--quiet Suppress success message
|
|
116
|
+
--version Show version
|
|
117
|
+
--help Show help
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
safe-env-check env.schema.js
|
|
121
|
+
safe-env-check --schema env.schema.js --strict
|
|
122
|
+
safe-env-check env.schema.js --env-file .env.production
|
|
123
|
+
safe-env-check env.schema.js --format json
|
|
124
|
+
safe-env-check --strict env.schema.js
|
|
125
|
+
`);
|
|
126
|
+
};
|
|
127
|
+
if (hasFlag("--help")) {
|
|
128
|
+
printHelp();
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
if (hasFlag("--version")) {
|
|
132
|
+
console.log(version);
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
var positionalArgs = getPositionalArgs(args);
|
|
136
|
+
var schemaFile = getArg("--schema") || positionalArgs[0];
|
|
137
|
+
if (!schemaFile) {
|
|
138
|
+
console.error("\u274C Schema file is required.\n");
|
|
139
|
+
printHelp();
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
var envFile = getArg("--env-file");
|
|
143
|
+
if (envFile) {
|
|
144
|
+
import_dotenv.default.config({ path: envFile });
|
|
145
|
+
}
|
|
146
|
+
var isStrict = hasFlag("--strict");
|
|
147
|
+
var isQuiet = hasFlag("--quiet");
|
|
148
|
+
var format = getArg("--format");
|
|
149
|
+
try {
|
|
150
|
+
const fullPath = import_path.default.resolve(process.cwd(), schemaFile);
|
|
151
|
+
if (!import_fs.default.existsSync(fullPath)) {
|
|
152
|
+
throw new Error(`Schema file not found: ${schemaFile}`);
|
|
153
|
+
}
|
|
154
|
+
const schema = require(fullPath);
|
|
155
|
+
validateEnv(schema, {
|
|
156
|
+
strict: isStrict,
|
|
157
|
+
formatError: (errors) => {
|
|
158
|
+
if (format === "json") {
|
|
159
|
+
return JSON.stringify({ errors }, null, 2);
|
|
160
|
+
}
|
|
161
|
+
return "\u274C Environment validation failed:\n" + errors.map((e) => `- ${e}`).join("\n");
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
if (!isQuiet) {
|
|
165
|
+
console.log("\u2705 Environment variables are valid");
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(error);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@safe-hand/safe-env-check",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/shshamim63/safe-env-check.git"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/your-username/your-repo#readme",
|
|
4
9
|
"main": "dist/index.js",
|
|
5
10
|
"types": "dist/index.d.ts",
|
|
6
11
|
"bin": {
|
|
7
12
|
"safe-env-check": "dist/cli.js"
|
|
8
13
|
},
|
|
9
14
|
"scripts": {
|
|
10
|
-
"build": "tsup src/index.ts --dts",
|
|
15
|
+
"build": "tsup src/index.ts src/cli.ts --dts",
|
|
11
16
|
"dev": "ts-node src/index.ts",
|
|
12
17
|
"test": "jest",
|
|
13
18
|
"lint": "tsc --noEmit"
|
package/src/cli.ts
CHANGED
|
@@ -1,20 +1,143 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
1
2
|
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { version } from "../package.json";
|
|
2
5
|
import { validateEnv } from "./validateEnv";
|
|
3
6
|
|
|
4
|
-
const
|
|
7
|
+
const args = process.argv.slice(2);
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Get value of a flag: --schema file, --env-file file, --format json
|
|
11
|
+
*/
|
|
12
|
+
const getArg = (flag: string): string | undefined => {
|
|
13
|
+
const index = args.indexOf(flag);
|
|
14
|
+
return index !== -1 ? args[index + 1] : undefined;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if flag exists
|
|
19
|
+
*/
|
|
20
|
+
const hasFlag = (flag: string) => args.includes(flag);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract positional (non-flag) arguments safely
|
|
24
|
+
*/
|
|
25
|
+
const getPositionalArgs = (args: string[]) => {
|
|
26
|
+
return args.filter((arg, index) => {
|
|
27
|
+
// remove flags
|
|
28
|
+
if (arg.startsWith("--")) return false;
|
|
29
|
+
|
|
30
|
+
// remove values of flags
|
|
31
|
+
const prev = args[index - 1];
|
|
32
|
+
if (prev === "--schema" || prev === "--env-file" || prev === "--format") {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const printHelp = () => {
|
|
41
|
+
console.log(`
|
|
42
|
+
safe-env-check
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
safe-env-check <schema-file> [options]
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--schema <file> Path to schema file
|
|
49
|
+
--strict Enable strict mode
|
|
50
|
+
--env-file <file> Load a custom .env file
|
|
51
|
+
--format json Output errors as JSON
|
|
52
|
+
--quiet Suppress success message
|
|
53
|
+
--version Show version
|
|
54
|
+
--help Show help
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
safe-env-check env.schema.js
|
|
58
|
+
safe-env-check --schema env.schema.js --strict
|
|
59
|
+
safe-env-check env.schema.js --env-file .env.production
|
|
60
|
+
safe-env-check env.schema.js --format json
|
|
61
|
+
safe-env-check --strict env.schema.js
|
|
62
|
+
`);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/* =======================
|
|
66
|
+
Early exit flags
|
|
67
|
+
======================= */
|
|
68
|
+
|
|
69
|
+
if (hasFlag("--help")) {
|
|
70
|
+
printHelp();
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (hasFlag("--version")) {
|
|
75
|
+
console.log(version);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* =======================
|
|
80
|
+
Resolve schema file
|
|
81
|
+
======================= */
|
|
82
|
+
|
|
83
|
+
const positionalArgs = getPositionalArgs(args);
|
|
84
|
+
const schemaFile = getArg("--schema") || positionalArgs[0];
|
|
85
|
+
|
|
86
|
+
if (!schemaFile) {
|
|
87
|
+
console.error("❌ Schema file is required.\n");
|
|
88
|
+
printHelp();
|
|
8
89
|
process.exit(1);
|
|
9
90
|
}
|
|
10
91
|
|
|
92
|
+
/* =======================
|
|
93
|
+
Load env file if provided
|
|
94
|
+
======================= */
|
|
95
|
+
|
|
96
|
+
const envFile = getArg("--env-file");
|
|
97
|
+
|
|
98
|
+
if (envFile) {
|
|
99
|
+
dotenv.config({ path: envFile });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* =======================
|
|
103
|
+
Flags
|
|
104
|
+
======================= */
|
|
105
|
+
|
|
106
|
+
const isStrict = hasFlag("--strict");
|
|
107
|
+
const isQuiet = hasFlag("--quiet");
|
|
108
|
+
const format = getArg("--format");
|
|
109
|
+
|
|
110
|
+
/* =======================
|
|
111
|
+
Main logic
|
|
112
|
+
======================= */
|
|
113
|
+
|
|
11
114
|
try {
|
|
12
|
-
const fullPath = path.resolve(process.cwd(),
|
|
115
|
+
const fullPath = path.resolve(process.cwd(), schemaFile);
|
|
116
|
+
|
|
117
|
+
if (!fs.existsSync(fullPath)) {
|
|
118
|
+
throw new Error(`Schema file not found: ${schemaFile}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
13
121
|
const schema = require(fullPath);
|
|
14
122
|
|
|
15
|
-
validateEnv(schema
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
123
|
+
validateEnv(schema, {
|
|
124
|
+
strict: isStrict,
|
|
125
|
+
formatError: (errors) => {
|
|
126
|
+
if (format === "json") {
|
|
127
|
+
return JSON.stringify({ errors }, null, 2);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
"❌ Environment validation failed:\n" +
|
|
132
|
+
errors.map((e) => `- ${e}`).join("\n")
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (!isQuiet) {
|
|
138
|
+
console.log("✅ Environment variables are valid");
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(error);
|
|
19
142
|
process.exit(1);
|
|
20
143
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mock validateEnv
|
|
5
|
+
*/
|
|
6
|
+
const mockValidateEnv = jest.fn();
|
|
7
|
+
|
|
8
|
+
jest.mock("../src/validateEnv", () => ({
|
|
9
|
+
validateEnv: mockValidateEnv,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mock fs
|
|
14
|
+
*/
|
|
15
|
+
jest.mock("fs", () => ({
|
|
16
|
+
existsSync: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Mock dotenv
|
|
21
|
+
*/
|
|
22
|
+
jest.mock("dotenv", () => ({
|
|
23
|
+
config: jest.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Mock package.json version
|
|
28
|
+
*/
|
|
29
|
+
jest.mock("../package.json", () => ({
|
|
30
|
+
version: "1.0.0",
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
describe("CLI", () => {
|
|
34
|
+
const originalArgv = process.argv;
|
|
35
|
+
const originalEnv = process.env;
|
|
36
|
+
|
|
37
|
+
let exitSpy: jest.SpyInstance;
|
|
38
|
+
let logSpy: jest.SpyInstance;
|
|
39
|
+
let errorSpy: jest.SpyInstance;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.resetModules();
|
|
43
|
+
|
|
44
|
+
process.argv = ["node", "cli.ts"];
|
|
45
|
+
process.env = { ...originalEnv };
|
|
46
|
+
|
|
47
|
+
exitSpy = jest.spyOn(process, "exit").mockImplementation((() => {
|
|
48
|
+
throw new Error("process.exit");
|
|
49
|
+
}) as never);
|
|
50
|
+
|
|
51
|
+
logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
52
|
+
errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
process.argv = originalArgv;
|
|
57
|
+
process.env = originalEnv;
|
|
58
|
+
jest.restoreAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("prints help when --help is passed", () => {
|
|
62
|
+
process.argv = ["node", "cli.ts", "--help"];
|
|
63
|
+
|
|
64
|
+
expect(() => require("../src/cli")).toThrow("process.exit");
|
|
65
|
+
|
|
66
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
67
|
+
expect(logSpy).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("prints version when --version is passed", () => {
|
|
71
|
+
process.argv = ["node", "cli.ts", "--version"];
|
|
72
|
+
|
|
73
|
+
expect(() => require("../src/cli")).toThrow("process.exit");
|
|
74
|
+
|
|
75
|
+
expect(logSpy).toHaveBeenCalledWith("1.0.0");
|
|
76
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("fails when schema file is missing", () => {
|
|
80
|
+
process.argv = ["node", "cli.ts"];
|
|
81
|
+
|
|
82
|
+
expect(() => require("../src/cli")).toThrow("process.exit");
|
|
83
|
+
|
|
84
|
+
expect(errorSpy).toHaveBeenCalledWith("❌ Schema file is required.\n");
|
|
85
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("fails if schema file does not exist", () => {
|
|
89
|
+
const fs = require("fs");
|
|
90
|
+
fs.existsSync.mockReturnValue(false);
|
|
91
|
+
|
|
92
|
+
process.argv = ["node", "cli.ts", "env.schema.js"];
|
|
93
|
+
|
|
94
|
+
expect(() => require("../src/cli")).toThrow("process.exit");
|
|
95
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("loads custom env file when --env-file is provided", () => {
|
|
99
|
+
const fs = require("fs");
|
|
100
|
+
const dotenv = require("dotenv");
|
|
101
|
+
|
|
102
|
+
fs.existsSync.mockReturnValue(true);
|
|
103
|
+
|
|
104
|
+
const schemaPath = path.resolve(process.cwd(), "env.schema.js");
|
|
105
|
+
|
|
106
|
+
jest.doMock(schemaPath, () => ({}), { virtual: true });
|
|
107
|
+
|
|
108
|
+
process.argv = [
|
|
109
|
+
"node",
|
|
110
|
+
"cli.ts",
|
|
111
|
+
"env.schema.js",
|
|
112
|
+
"--env-file",
|
|
113
|
+
".env.production",
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
mockValidateEnv.mockImplementation(() => ({}));
|
|
117
|
+
|
|
118
|
+
require("../src/cli");
|
|
119
|
+
|
|
120
|
+
expect(dotenv.config).toHaveBeenCalledWith({
|
|
121
|
+
path: ".env.production",
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("calls validateEnv with correct options", () => {
|
|
126
|
+
const fs = require("fs");
|
|
127
|
+
fs.existsSync.mockReturnValue(true);
|
|
128
|
+
|
|
129
|
+
const schemaPath = path.resolve(process.cwd(), "env.schema.js");
|
|
130
|
+
|
|
131
|
+
jest.doMock(
|
|
132
|
+
schemaPath,
|
|
133
|
+
() => ({
|
|
134
|
+
PORT: { type: "number", required: true },
|
|
135
|
+
}),
|
|
136
|
+
{ virtual: true },
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
process.argv = [
|
|
140
|
+
"node",
|
|
141
|
+
"cli.ts",
|
|
142
|
+
"env.schema.js",
|
|
143
|
+
"--strict",
|
|
144
|
+
"--format",
|
|
145
|
+
"json",
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
mockValidateEnv.mockImplementation(() => ({}));
|
|
149
|
+
|
|
150
|
+
require("../src/cli");
|
|
151
|
+
|
|
152
|
+
expect(mockValidateEnv).toHaveBeenCalledWith(
|
|
153
|
+
expect.any(Object),
|
|
154
|
+
expect.objectContaining({
|
|
155
|
+
strict: true,
|
|
156
|
+
formatError: expect.any(Function),
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(logSpy).toHaveBeenCalledWith("✅ Environment variables are valid");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("suppresses success message with --quiet", () => {
|
|
164
|
+
const fs = require("fs");
|
|
165
|
+
fs.existsSync.mockReturnValue(true);
|
|
166
|
+
|
|
167
|
+
const schemaPath = path.resolve(process.cwd(), "env.schema.js");
|
|
168
|
+
|
|
169
|
+
jest.doMock(schemaPath, () => ({}), { virtual: true });
|
|
170
|
+
|
|
171
|
+
process.argv = ["node", "cli.ts", "env.schema.js", "--quiet"];
|
|
172
|
+
|
|
173
|
+
mockValidateEnv.mockImplementation(() => ({}));
|
|
174
|
+
|
|
175
|
+
require("../src/cli");
|
|
176
|
+
|
|
177
|
+
expect(logSpy).not.toHaveBeenCalledWith(
|
|
178
|
+
"✅ Environment variables are valid",
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("prints formatted error when validateEnv throws", () => {
|
|
183
|
+
const fs = require("fs");
|
|
184
|
+
fs.existsSync.mockReturnValue(true);
|
|
185
|
+
|
|
186
|
+
const schemaPath = path.resolve(process.cwd(), "env.schema.js");
|
|
187
|
+
|
|
188
|
+
jest.doMock(schemaPath, () => ({}), { virtual: true });
|
|
189
|
+
|
|
190
|
+
process.argv = ["node", "cli.ts", "env.schema.js", "--format", "json"];
|
|
191
|
+
|
|
192
|
+
mockValidateEnv.mockImplementation(() => {
|
|
193
|
+
throw new Error(JSON.stringify({ errors: ["PORT is required"] }));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(() => require("../src/cli")).toThrow("process.exit");
|
|
197
|
+
|
|
198
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
199
|
+
});
|
|
200
|
+
});
|