@wchen.ai/env-from-example 1.0.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/README.md +240 -0
- package/dist/env-from-example.js +416 -0
- package/dist/setup-env.js +473 -0
- package/dist/src/parse.js +395 -0
- package/dist/src/polish.js +369 -0
- package/dist/src/schema.js +67 -0
- package/dist/src/validate.js +255 -0
- package/dist/src/version.js +35 -0
- package/dist/test/integration/cli.test.js +451 -0
- package/dist/test/unit/env-from-example.test.js +846 -0
- package/dist/test/unit/setup-env.test.js +236 -0
- package/dist/vitest.config.js +15 -0
- package/package.json +65 -0
- package/schema.json +263 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { parseEnvExample, serializeEnvExample } from "./parse.js";
|
|
4
|
+
function parseSemver(s) {
|
|
5
|
+
const parts = s.replace(/^v/i, "").split(".");
|
|
6
|
+
const major = Math.max(0, parseInt(parts[0] || "0", 10) || 0);
|
|
7
|
+
const minor = Math.max(0, parseInt(parts[1] || "0", 10) || 0);
|
|
8
|
+
const patch = Math.max(0, parseInt(parts[2] || "0", 10) || 0);
|
|
9
|
+
return [major, minor, patch];
|
|
10
|
+
}
|
|
11
|
+
export function bumpSemver(current, bump) {
|
|
12
|
+
const [major, minor, patch] = parseSemver(current);
|
|
13
|
+
if (bump === "major")
|
|
14
|
+
return `${major + 1}.0.0`;
|
|
15
|
+
if (bump === "minor")
|
|
16
|
+
return `${major}.${minor + 1}.0`;
|
|
17
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
18
|
+
}
|
|
19
|
+
export function updateEnvSchemaVersion(rootDir, newVersion, options = {}) {
|
|
20
|
+
const envExamplePath = path.join(rootDir, ".env.example");
|
|
21
|
+
if (!fs.existsSync(envExamplePath)) {
|
|
22
|
+
throw new Error(`.env.example not found at ${envExamplePath}`);
|
|
23
|
+
}
|
|
24
|
+
const { variables } = parseEnvExample(rootDir);
|
|
25
|
+
const content = serializeEnvExample(newVersion, variables);
|
|
26
|
+
fs.writeFileSync(envExamplePath, content, "utf-8");
|
|
27
|
+
if (options.syncPackage) {
|
|
28
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
29
|
+
if (fs.existsSync(pkgPath)) {
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
31
|
+
pkg.version = newVersion;
|
|
32
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterEach } from "vitest";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
const PROJECT_ROOT = path.resolve(__dirname, "../..");
|
|
6
|
+
const FIXTURES_DIR = path.join(PROJECT_ROOT, "test", "fixtures");
|
|
7
|
+
const FULL_FIXTURE_DIR = path.join(FIXTURES_DIR, "full");
|
|
8
|
+
const FULL_FIXTURE_ENV_EXAMPLE = path.join(FULL_FIXTURE_DIR, ".env.example");
|
|
9
|
+
const CLI_PATH = path.join(PROJECT_ROOT, "dist", "env-from-example.js");
|
|
10
|
+
const FULL_FIXTURE_CONTENT = `# ==============================================
|
|
11
|
+
# Environment Variables
|
|
12
|
+
# ==============================================
|
|
13
|
+
# env-from-example (https://www.npmjs.com/package/env-from-example)
|
|
14
|
+
# ==============================================
|
|
15
|
+
|
|
16
|
+
# ENV_SCHEMA_VERSION="1.0"
|
|
17
|
+
|
|
18
|
+
# ------ Database ------
|
|
19
|
+
# [REQUIRED] Postgres connection string
|
|
20
|
+
DATABASE_URL=postgres://localhost:5432/myapp
|
|
21
|
+
|
|
22
|
+
# Pool size (number); default is fine for dev
|
|
23
|
+
DATABASE_POOL_SIZE=10
|
|
24
|
+
|
|
25
|
+
# ------ API ------
|
|
26
|
+
# Default: (empty)
|
|
27
|
+
API_KEY=
|
|
28
|
+
|
|
29
|
+
# Secret for signing; no default
|
|
30
|
+
API_SECRET=
|
|
31
|
+
|
|
32
|
+
# Base URL, can contain spaces or special chars when quoted
|
|
33
|
+
API_BASE_URL=https://api.example.com/v1
|
|
34
|
+
|
|
35
|
+
# ------ App ------
|
|
36
|
+
# [TYPE: structured/enum]
|
|
37
|
+
NODE_ENV=development
|
|
38
|
+
|
|
39
|
+
# Session secret: auto-generated if left empty (64-byte base64)
|
|
40
|
+
SESSION_SECRET=
|
|
41
|
+
|
|
42
|
+
# Optional feature flag (commented out = included with default, not prompted)
|
|
43
|
+
# FEATURE_BETA=false
|
|
44
|
+
|
|
45
|
+
# Optional port; comment line = variable still in schema with default
|
|
46
|
+
# PORT=3000
|
|
47
|
+
`;
|
|
48
|
+
function runCli(args, _cwd) {
|
|
49
|
+
const result = spawnSync("node", [CLI_PATH, ...args], {
|
|
50
|
+
cwd: PROJECT_ROOT,
|
|
51
|
+
env: { ...process.env, PATH: process.env.PATH },
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
timeout: 15000,
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
status: result.status,
|
|
57
|
+
stdout: result.stdout ?? "",
|
|
58
|
+
stderr: result.stderr ?? "",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
describe("CLI integration", () => {
|
|
62
|
+
beforeAll(() => {
|
|
63
|
+
if (!fs.existsSync(CLI_PATH)) {
|
|
64
|
+
throw new Error(`CLI not built at ${CLI_PATH}. Run "pnpm run build" before integration tests.`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
beforeAll(() => {
|
|
68
|
+
fs.mkdirSync(FULL_FIXTURE_DIR, { recursive: true });
|
|
69
|
+
fs.writeFileSync(FULL_FIXTURE_ENV_EXAMPLE, FULL_FIXTURE_CONTENT, "utf-8");
|
|
70
|
+
});
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
fs.writeFileSync(FULL_FIXTURE_ENV_EXAMPLE, FULL_FIXTURE_CONTENT, "utf-8");
|
|
73
|
+
const envFiles = [
|
|
74
|
+
path.join(FIXTURES_DIR, "full", ".env"),
|
|
75
|
+
path.join(FIXTURES_DIR, "full", ".env.test"),
|
|
76
|
+
path.join(FIXTURES_DIR, "minimal", ".env"),
|
|
77
|
+
path.join(FIXTURES_DIR, "required-only", ".env"),
|
|
78
|
+
];
|
|
79
|
+
for (const p of envFiles) {
|
|
80
|
+
try {
|
|
81
|
+
fs.unlinkSync(p);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* ignore */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
it("exits with error when .env.example is missing", () => {
|
|
89
|
+
const result = runCli(["-y", "--cwd", "/nonexistent/dir"], PROJECT_ROOT);
|
|
90
|
+
expect(result.status).not.toBe(0);
|
|
91
|
+
expect(result.stderr).toMatch(/No \.env\.example found/);
|
|
92
|
+
});
|
|
93
|
+
it("creates .env with -y (non-interactive) from full fixture", () => {
|
|
94
|
+
const fixtureDir = path.join(FIXTURES_DIR, "full");
|
|
95
|
+
const result = runCli(["-y", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
96
|
+
expect(result.status).toBe(0);
|
|
97
|
+
expect(result.stdout).toMatch(/successfully created\/updated/);
|
|
98
|
+
const envPath = path.join(fixtureDir, ".env");
|
|
99
|
+
expect(fs.existsSync(envPath)).toBe(true);
|
|
100
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
101
|
+
expect(content).toMatch(/# ENV_SCHEMA_VERSION="1.0"/);
|
|
102
|
+
expect(content).toMatch(/DATABASE_URL=postgres:\/\/localhost:5432\/myapp/);
|
|
103
|
+
expect(content).toMatch(/NODE_ENV=development/);
|
|
104
|
+
expect(content).toMatch(/SESSION_SECRET=/);
|
|
105
|
+
});
|
|
106
|
+
it("creates .env.test with -y -e test", () => {
|
|
107
|
+
const fixtureDir = path.join(FIXTURES_DIR, "full");
|
|
108
|
+
const result = runCli(["-y", "-e", "test", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
109
|
+
expect(result.status).toBe(0);
|
|
110
|
+
const envPath = path.join(fixtureDir, ".env.test");
|
|
111
|
+
expect(fs.existsSync(envPath)).toBe(true);
|
|
112
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
113
|
+
expect(content).toMatch(/DATABASE_URL=/);
|
|
114
|
+
expect(content).toMatch(/NODE_ENV=/);
|
|
115
|
+
});
|
|
116
|
+
it("preserves section headers in generated .env", () => {
|
|
117
|
+
const fixtureDir = path.join(FIXTURES_DIR, "full");
|
|
118
|
+
runCli(["-y", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
119
|
+
const content = fs.readFileSync(path.join(fixtureDir, ".env"), "utf-8");
|
|
120
|
+
expect(content).toMatch(/#\s+Database/);
|
|
121
|
+
expect(content).toMatch(/#\s+API/);
|
|
122
|
+
expect(content).toMatch(/#\s+App/);
|
|
123
|
+
});
|
|
124
|
+
it("uses CLI override when provided", () => {
|
|
125
|
+
const fixtureDir = path.join(FIXTURES_DIR, "full");
|
|
126
|
+
runCli([
|
|
127
|
+
"-y",
|
|
128
|
+
"--cwd",
|
|
129
|
+
fixtureDir,
|
|
130
|
+
"--database-url",
|
|
131
|
+
"postgres://custom:5432/db",
|
|
132
|
+
], PROJECT_ROOT);
|
|
133
|
+
const content = fs.readFileSync(path.join(fixtureDir, ".env"), "utf-8");
|
|
134
|
+
expect(content).toMatch(/DATABASE_URL=postgres:\/\/custom:5432\/db/);
|
|
135
|
+
});
|
|
136
|
+
it("minimal fixture: creates .env with version and vars", () => {
|
|
137
|
+
const fixtureDir = path.join(FIXTURES_DIR, "minimal");
|
|
138
|
+
const result = runCli(["-y", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
139
|
+
expect(result.status).toBe(0);
|
|
140
|
+
const content = fs.readFileSync(path.join(fixtureDir, ".env"), "utf-8");
|
|
141
|
+
expect(content).toMatch(/# ENV_SCHEMA_VERSION="2.0"/);
|
|
142
|
+
expect(content).toMatch(/NODE_ENV=development/);
|
|
143
|
+
expect(content).toMatch(/SOME_KEY=default_value/);
|
|
144
|
+
});
|
|
145
|
+
it("required-only fixture: creates .env with empty required var", () => {
|
|
146
|
+
const fixtureDir = path.join(FIXTURES_DIR, "required-only");
|
|
147
|
+
const result = runCli(["-y", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
148
|
+
expect(result.status).toBe(0);
|
|
149
|
+
expect(result.stdout).toMatch(/successfully created\/updated/);
|
|
150
|
+
const content = fs.readFileSync(path.join(fixtureDir, ".env"), "utf-8");
|
|
151
|
+
expect(content).toMatch(/REQUIRED_VAR=/);
|
|
152
|
+
});
|
|
153
|
+
it("--polish -y normalizes .env.example (non-interactive)", () => {
|
|
154
|
+
const fixtureDir = path.join(FIXTURES_DIR, "full");
|
|
155
|
+
const envPath = path.join(fixtureDir, ".env.example");
|
|
156
|
+
const before = fs.readFileSync(envPath, "utf-8");
|
|
157
|
+
const result = runCli(["--polish", "-y", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
158
|
+
expect(result.status).toBe(0);
|
|
159
|
+
expect(result.stdout).toMatch(/\.env\.example polished/);
|
|
160
|
+
const after = fs.readFileSync(envPath, "utf-8");
|
|
161
|
+
expect(after).toMatch(/# ENV_SCHEMA_VERSION="1.0"/);
|
|
162
|
+
expect(after).toMatch(/DATABASE_URL=postgres:\/\/localhost:5432\/myapp/);
|
|
163
|
+
fs.writeFileSync(envPath, before, "utf-8");
|
|
164
|
+
});
|
|
165
|
+
it("--polish -y enriches comments with Default: and preserves sections", () => {
|
|
166
|
+
const fixtureDir = path.join(FIXTURES_DIR, "full");
|
|
167
|
+
const envPath = path.join(fixtureDir, ".env.example");
|
|
168
|
+
const before = fs.readFileSync(envPath, "utf-8");
|
|
169
|
+
try {
|
|
170
|
+
runCli(["--polish", "-y", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
171
|
+
const after = fs.readFileSync(envPath, "utf-8");
|
|
172
|
+
expect(after).toMatch(/Default: postgres:\/\/localhost:5432\/myapp/);
|
|
173
|
+
expect(after).toMatch(/#\s+Database/);
|
|
174
|
+
expect(after).toMatch(/#\s+API/);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
fs.writeFileSync(envPath, before, "utf-8");
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
it("--polish exits with error when .env.example is missing", () => {
|
|
181
|
+
const result = runCli(["--polish", "--cwd", "/nonexistent/dir"], PROJECT_ROOT);
|
|
182
|
+
expect(result.status).not.toBe(0);
|
|
183
|
+
expect(result.stderr).toMatch(/\.env\.example not found/);
|
|
184
|
+
});
|
|
185
|
+
it("--version updates ENV_SCHEMA_VERSION", () => {
|
|
186
|
+
const fixtureDir = path.join(FIXTURES_DIR, "minimal");
|
|
187
|
+
const envPath = path.join(fixtureDir, ".env.example");
|
|
188
|
+
const before = fs.readFileSync(envPath, "utf-8");
|
|
189
|
+
const result = runCli(["--version", "9.9.9", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
190
|
+
expect(result.status).toBe(0);
|
|
191
|
+
expect(result.stdout).toMatch(/ENV_SCHEMA_VERSION set to 9.9.9/);
|
|
192
|
+
const after = fs.readFileSync(envPath, "utf-8");
|
|
193
|
+
expect(after).toMatch(/# ENV_SCHEMA_VERSION="9.9.9"/);
|
|
194
|
+
fs.writeFileSync(envPath, before, "utf-8");
|
|
195
|
+
});
|
|
196
|
+
it("--validate exits 0 when .env conforms to schema", () => {
|
|
197
|
+
const fixtureDir = path.join(FIXTURES_DIR, "full");
|
|
198
|
+
const envPath = path.join(fixtureDir, ".env");
|
|
199
|
+
fs.writeFileSync(envPath, '# ENV_SCHEMA_VERSION="1.0"\nDATABASE_URL=postgres://localhost:5432/myapp\nNODE_ENV=development\n', "utf-8");
|
|
200
|
+
try {
|
|
201
|
+
const result = runCli(["--validate", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
202
|
+
expect(result.status).toBe(0);
|
|
203
|
+
expect(result.stdout).toMatch(/valid against .env.example schema/);
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
try {
|
|
207
|
+
fs.unlinkSync(envPath);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
/* ignore */
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
it("--validate exits 1 when required var is missing", () => {
|
|
215
|
+
const fixtureDir = path.join(FIXTURES_DIR, "full");
|
|
216
|
+
const envPath = path.join(fixtureDir, ".env");
|
|
217
|
+
fs.writeFileSync(envPath, "NODE_ENV=development\n", "utf-8");
|
|
218
|
+
try {
|
|
219
|
+
const result = runCli(["--validate", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
220
|
+
expect(result.status).not.toBe(0);
|
|
221
|
+
expect(result.stderr).toMatch(/DATABASE_URL|required/);
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
try {
|
|
225
|
+
fs.unlinkSync(envPath);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
/* ignore */
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
it("--init creates .env.example from scratch", () => {
|
|
233
|
+
const tmpDir = path.join(FIXTURES_DIR, "..", "fixtures-cli-init");
|
|
234
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
235
|
+
const envExamplePath = path.join(tmpDir, ".env.example");
|
|
236
|
+
try {
|
|
237
|
+
const result = runCli(["--init", "-y", "--cwd", tmpDir], PROJECT_ROOT);
|
|
238
|
+
expect(result.status).toBe(0);
|
|
239
|
+
expect(result.stdout).toMatch(/\.env\.example created/);
|
|
240
|
+
expect(fs.existsSync(envExamplePath)).toBe(true);
|
|
241
|
+
const content = fs.readFileSync(envExamplePath, "utf-8");
|
|
242
|
+
expect(content).toMatch(/NODE_ENV/);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
try {
|
|
246
|
+
fs.unlinkSync(envExamplePath);
|
|
247
|
+
fs.rmdirSync(tmpDir);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
/* ignore */
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
it("--init fails when .env.example already exists", () => {
|
|
255
|
+
const fixtureDir = path.join(FIXTURES_DIR, "full");
|
|
256
|
+
const result = runCli(["--init", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
257
|
+
expect(result.status).not.toBe(0);
|
|
258
|
+
expect(result.stderr).toMatch(/already exists/);
|
|
259
|
+
});
|
|
260
|
+
it("--dry-run previews output without writing file", () => {
|
|
261
|
+
const fixtureDir = path.join(FIXTURES_DIR, "full");
|
|
262
|
+
const envPath = path.join(fixtureDir, ".env");
|
|
263
|
+
try {
|
|
264
|
+
const result = runCli(["-y", "--dry-run", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
265
|
+
expect(result.status).toBe(0);
|
|
266
|
+
expect(result.stdout).toMatch(/Dry run/);
|
|
267
|
+
expect(result.stdout).toMatch(/DATABASE_URL=/);
|
|
268
|
+
expect(fs.existsSync(envPath)).toBe(false);
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
try {
|
|
272
|
+
fs.unlinkSync(envPath);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
/* ignore */
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
it("shows helpful error when .env.example is missing", () => {
|
|
280
|
+
const result = runCli(["-y", "--cwd", "/nonexistent/dir"], PROJECT_ROOT);
|
|
281
|
+
expect(result.status).not.toBe(0);
|
|
282
|
+
expect(result.stderr).toMatch(/--init/);
|
|
283
|
+
expect(result.stderr).toMatch(/To get started/);
|
|
284
|
+
});
|
|
285
|
+
it("shows post-generation summary", () => {
|
|
286
|
+
const fixtureDir = path.join(FIXTURES_DIR, "full");
|
|
287
|
+
const result = runCli(["-y", "--cwd", fixtureDir], PROJECT_ROOT);
|
|
288
|
+
expect(result.status).toBe(0);
|
|
289
|
+
expect(result.stdout).toMatch(/variables configured/);
|
|
290
|
+
});
|
|
291
|
+
it("--validate passes when hex_color values match schema pattern", () => {
|
|
292
|
+
const tmpDir = path.join(FIXTURES_DIR, "..", "fixtures-hex-valid");
|
|
293
|
+
const envExamplePath = path.join(tmpDir, ".env.example");
|
|
294
|
+
const envPath = path.join(tmpDir, ".env");
|
|
295
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
296
|
+
fs.writeFileSync(envExamplePath, "# Hex color [TYPE: visual/hex_color]\nBRAND_COLOR=#000000\n", "utf-8");
|
|
297
|
+
fs.writeFileSync(envPath, "BRAND_COLOR=#3b82f6\n", "utf-8");
|
|
298
|
+
try {
|
|
299
|
+
const result = runCli(["--validate", "--cwd", tmpDir], PROJECT_ROOT);
|
|
300
|
+
expect(result.status).toBe(0);
|
|
301
|
+
expect(result.stdout).toMatch(/valid/);
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
try {
|
|
305
|
+
fs.unlinkSync(envPath);
|
|
306
|
+
fs.unlinkSync(envExamplePath);
|
|
307
|
+
fs.rmdirSync(tmpDir);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
/* ignore */
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
it("--validate fails when hex_color value does not match", () => {
|
|
315
|
+
const tmpDir = path.join(FIXTURES_DIR, "..", "fixtures-hex-invalid");
|
|
316
|
+
const envExamplePath = path.join(tmpDir, ".env.example");
|
|
317
|
+
const envPath = path.join(tmpDir, ".env");
|
|
318
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
319
|
+
fs.writeFileSync(envExamplePath, "# Hex color [TYPE: visual/hex_color]\nBRAND_COLOR=#000000\n", "utf-8");
|
|
320
|
+
fs.writeFileSync(envPath, "BRAND_COLOR=not-a-color\n", "utf-8");
|
|
321
|
+
try {
|
|
322
|
+
const result = runCli(["--validate", "--cwd", tmpDir], PROJECT_ROOT);
|
|
323
|
+
expect(result.status).not.toBe(0);
|
|
324
|
+
expect(result.stderr).toMatch(/valid visual\/hex_color/);
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
try {
|
|
328
|
+
fs.unlinkSync(envPath);
|
|
329
|
+
fs.unlinkSync(envExamplePath);
|
|
330
|
+
fs.rmdirSync(tmpDir);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
/* ignore */
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
it("--validate passes for structured/enum with matching value", () => {
|
|
338
|
+
const tmpDir = path.join(FIXTURES_DIR, "..", "fixtures-enum-valid");
|
|
339
|
+
const envExamplePath = path.join(tmpDir, ".env.example");
|
|
340
|
+
const envPath = path.join(tmpDir, ".env");
|
|
341
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
342
|
+
fs.writeFileSync(envExamplePath, "# Log level [TYPE: structured/enum] [CONSTRAINTS: pattern=^(debug|info|warn|error)$]\nLOG_LEVEL=info\n", "utf-8");
|
|
343
|
+
fs.writeFileSync(envPath, "LOG_LEVEL=debug\n", "utf-8");
|
|
344
|
+
try {
|
|
345
|
+
const result = runCli(["--validate", "--cwd", tmpDir], PROJECT_ROOT);
|
|
346
|
+
expect(result.status).toBe(0);
|
|
347
|
+
expect(result.stdout).toMatch(/valid/);
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
try {
|
|
351
|
+
fs.unlinkSync(envPath);
|
|
352
|
+
fs.unlinkSync(envExamplePath);
|
|
353
|
+
fs.rmdirSync(tmpDir);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
/* ignore */
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
it("--validate fails for structured/enum with non-matching value", () => {
|
|
361
|
+
const tmpDir = path.join(FIXTURES_DIR, "..", "fixtures-enum-invalid");
|
|
362
|
+
const envExamplePath = path.join(tmpDir, ".env.example");
|
|
363
|
+
const envPath = path.join(tmpDir, ".env");
|
|
364
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
365
|
+
fs.writeFileSync(envExamplePath, "# Log level [TYPE: structured/enum] [CONSTRAINTS: pattern=^(debug|info|warn|error)$]\nLOG_LEVEL=info\n", "utf-8");
|
|
366
|
+
fs.writeFileSync(envPath, "LOG_LEVEL=verbose\n", "utf-8");
|
|
367
|
+
try {
|
|
368
|
+
const result = runCli(["--validate", "--cwd", tmpDir], PROJECT_ROOT);
|
|
369
|
+
expect(result.status).not.toBe(0);
|
|
370
|
+
expect(result.stderr).toMatch(/one of/);
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
try {
|
|
374
|
+
fs.unlinkSync(envPath);
|
|
375
|
+
fs.unlinkSync(envExamplePath);
|
|
376
|
+
fs.rmdirSync(tmpDir);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
/* ignore */
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
it("--polish -y preserves [TYPE] and [CONSTRAINTS] annotations", () => {
|
|
384
|
+
const tmpDir = path.join(FIXTURES_DIR, "..", "fixtures-constraints-polish");
|
|
385
|
+
const envExamplePath = path.join(tmpDir, ".env.example");
|
|
386
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
387
|
+
fs.writeFileSync(envExamplePath, "# Server port [TYPE: integer] [CONSTRAINTS: min=1,max=65535]\nPORT=3000\n", "utf-8");
|
|
388
|
+
try {
|
|
389
|
+
const result = runCli(["--polish", "-y", "--cwd", tmpDir], PROJECT_ROOT);
|
|
390
|
+
expect(result.status).toBe(0);
|
|
391
|
+
const after = fs.readFileSync(envExamplePath, "utf-8");
|
|
392
|
+
expect(after).toMatch(/\[TYPE: integer\]/);
|
|
393
|
+
expect(after).toMatch(/\[CONSTRAINTS: min=1,max=65535\]/);
|
|
394
|
+
expect(after).toMatch(/PORT=3000/);
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
try {
|
|
398
|
+
fs.unlinkSync(envExamplePath);
|
|
399
|
+
fs.rmdirSync(tmpDir);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
/* ignore */
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
it("--validate with integer constraints constraints", () => {
|
|
407
|
+
const tmpDir = path.join(FIXTURES_DIR, "..", "fixtures-int-constraints");
|
|
408
|
+
const envExamplePath = path.join(tmpDir, ".env.example");
|
|
409
|
+
const envPath = path.join(tmpDir, ".env");
|
|
410
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
411
|
+
fs.writeFileSync(envExamplePath, "# Port [TYPE: integer] [CONSTRAINTS: min=1,max=65535]\nPORT=3000\n", "utf-8");
|
|
412
|
+
fs.writeFileSync(envPath, "PORT=8080\n", "utf-8");
|
|
413
|
+
try {
|
|
414
|
+
const result = runCli(["--validate", "--cwd", tmpDir], PROJECT_ROOT);
|
|
415
|
+
expect(result.status).toBe(0);
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
try {
|
|
419
|
+
fs.unlinkSync(envPath);
|
|
420
|
+
fs.unlinkSync(envExamplePath);
|
|
421
|
+
fs.rmdirSync(tmpDir);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
/* ignore */
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
it("--validate fails for integer outside constraints bounds", () => {
|
|
429
|
+
const tmpDir = path.join(FIXTURES_DIR, "..", "fixtures-int-oob");
|
|
430
|
+
const envExamplePath = path.join(tmpDir, ".env.example");
|
|
431
|
+
const envPath = path.join(tmpDir, ".env");
|
|
432
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
433
|
+
fs.writeFileSync(envExamplePath, "# Port [TYPE: integer] [CONSTRAINTS: min=1,max=65535]\nPORT=3000\n", "utf-8");
|
|
434
|
+
fs.writeFileSync(envPath, "PORT=0\n", "utf-8");
|
|
435
|
+
try {
|
|
436
|
+
const result = runCli(["--validate", "--cwd", tmpDir], PROJECT_ROOT);
|
|
437
|
+
expect(result.status).not.toBe(0);
|
|
438
|
+
expect(result.stderr).toMatch(/>= 1/);
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
try {
|
|
442
|
+
fs.unlinkSync(envPath);
|
|
443
|
+
fs.unlinkSync(envExamplePath);
|
|
444
|
+
fs.rmdirSync(tmpDir);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
/* ignore */
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
});
|