Compare commits

...

3 Commits

Author SHA1 Message Date
Claude Bot
794073d62f Disable environment variable expansion in process.loadEnvFile
- Change Parser.parse expand parameter from true to false
- Update test to verify that variable expansion is disabled
- Variables like $VAR and ${VAR} are now treated as literal strings
- All tests still pass (7/7)

This ensures process.loadEnvFile behaves as a simple key-value parser
without performing variable substitution.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 03:58:19 +00:00
autofix-ci[bot]
082d9d61bf [autofix.ci] apply automated fixes 2025-08-28 03:52:07 +00:00
Claude Bot
8391ea10e3 Implement process.loadEnvFile for Node.js compatibility
Add process.loadEnvFile() method that loads environment variables from .env files
and returns them as a JavaScript object.

Features:
- Loads and parses .env files with support for:
  - Basic key=value pairs
  - Quoted strings (single and double quotes)
  - Multiline values
  - Variable expansion
  - Export statements
  - Comments and empty lines
- Returns parsed variables as a JavaScript object
- Proper error handling for invalid files or arguments
- Memory-safe implementation using VM allocator

Implementation:
- Added jsFunctionProcessLoadEnvFile to BunProcess.cpp function table
- Implemented parsing logic in src/env_loader.zig using existing env parser
- Added comprehensive test suite covering all supported features
- All tests pass (7/7)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 03:50:27 +00:00
3 changed files with 192 additions and 0 deletions

View File

@@ -148,6 +148,7 @@ extern "C" bool Bun__GlobalObject__hasIPC(JSGlobalObject*);
extern "C" bool Bun__ensureProcessIPCInitialized(JSGlobalObject*);
extern "C" const char* Bun__githubURL;
BUN_DECLARE_HOST_FUNCTION(Bun__Process__send);
BUN_DECLARE_HOST_FUNCTION(jsFunctionProcessLoadEnvFile);
extern "C" void Process__emitDisconnectEvent(Zig::GlobalObject* global);
extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValue value);
@@ -3861,6 +3862,7 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu
hrtime constructProcessHrtimeObject PropertyCallback
isBun constructIsBun PropertyCallback
kill Process_functionKill Function 2
loadEnvFile jsFunctionProcessLoadEnvFile Function 1
mainModule processObjectInternalsMainModuleCodeGenerator Builtin|Accessor
memoryUsage constructMemoryUsage PropertyCallback
moduleLoadList Process_stubEmptyArray PropertyCallback

View File

@@ -1330,12 +1330,64 @@ pub const Map = struct {
}
};
/// JavaScript function to load environment variables from a .env file
/// Takes a file path as argument and returns an object containing the parsed env vars
pub fn jsFunctionProcessLoadEnvFile(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const vm = globalObject.bunVM();
const arguments = callframe.arguments_old(1).slice();
var args = jsc.CallFrame.ArgumentsSlice.init(vm, arguments);
defer args.deinit();
const path_like = (try jsc.Node.PathLike.fromJS(globalObject, &args)) orelse {
return globalObject.throwInvalidArguments("loadEnvFile requires a file path argument", .{});
};
const path_slice = path_like.slice();
defer path_like.deinit();
// Use bun.sys.File.toSource to read the file
const source = bun.sys.File.toSource(path_slice, bun.default_allocator, .{}).unwrap() catch |err| {
return globalObject.throwPretty("Failed to read env file '{s}': {s}", .{ path_slice, @errorName(err) });
};
defer bun.default_allocator.free(source.contents);
// Create a temporary map for parsing - using VM allocator instead
var env_map = Map.init(vm.allocator);
defer env_map.map.deinit();
// Parse the env file using the existing parser - using VM allocator
var value_buffer = std.ArrayList(u8).init(bun.default_allocator);
defer value_buffer.deinit();
Parser.parse(&source, vm.allocator, &env_map, &value_buffer, false, false, false) catch |err| {
return globalObject.throwPretty("Failed to parse env file '{s}': {s}", .{ path_slice, @errorName(err) });
};
// Convert the parsed map to a JavaScript object
const result_object = jsc.JSValue.createEmptyObject(globalObject, 0);
var iter = env_map.iterator();
while (iter.next()) |entry| {
const key_str = jsc.ZigString.init(entry.key_ptr.*);
const value_js = bun.String.createUTF8ForJS(globalObject, entry.value_ptr.value) catch {
return globalObject.throwOutOfMemory();
};
result_object.put(globalObject, key_str, value_js);
}
return result_object;
}
pub var instance: ?*Loader = null;
pub const home_env = if (Environment.isWindows) "USERPROFILE" else "HOME";
const string = []const u8;
// Export the function for C binding
comptime {
@export(&jsc.toJSHostFn(jsFunctionProcessLoadEnvFile), .{ .name = "jsFunctionProcessLoadEnvFile" });
}
const Fs = @import("./fs.zig");
const std = @import("std");
const URL = @import("./url.zig").URL;
@@ -1346,6 +1398,7 @@ const Environment = bun.Environment;
const OOM = bun.OOM;
const Output = bun.Output;
const analytics = bun.analytics;
const jsc = bun.jsc;
const logger = bun.logger;
const s3 = bun.S3;
const strings = bun.strings;

View File

@@ -0,0 +1,137 @@
import { describe, expect, it } from "bun:test";
import { unlinkSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
describe("process.loadEnvFile", () => {
it("should load environment variables from a .env file", () => {
const tempDir = tmpdir();
const envFile = join(tempDir, "test.env");
// Create a test .env file
const envContent = `
FOO=bar
BAZ=qux
MULTILINE="line1
line2"
QUOTED='single quoted'
EMPTY=
`;
writeFileSync(envFile, envContent);
try {
const result = process.loadEnvFile(envFile);
expect(result).toEqual({
FOO: "bar",
BAZ: "qux",
MULTILINE: "line1\nline2",
QUOTED: "single quoted",
EMPTY: "",
});
} finally {
unlinkSync(envFile);
}
});
it("should NOT expand environment variables (expansion disabled)", () => {
const tempDir = tmpdir();
const envFile = join(tempDir, "test-no-expand.env");
// Create a test .env file with variable expansion syntax
const envContent = `
BASE_URL=https://example.com
API_URL=$BASE_URL/api
FULL_URL=\${API_URL}/v1
WITH_DEFAULT=\${MISSING_VAR:-default}
`;
writeFileSync(envFile, envContent);
try {
const result = process.loadEnvFile(envFile);
// Variable expansion should be disabled, so variables should remain as literal strings
expect(result).toEqual({
BASE_URL: "https://example.com",
API_URL: "$BASE_URL/api", // Should NOT be expanded
FULL_URL: "${API_URL}/v1", // Should NOT be expanded
WITH_DEFAULT: "${MISSING_VAR:-default}", // Should NOT be expanded
});
} finally {
unlinkSync(envFile);
}
});
it("should handle export statements", () => {
const tempDir = tmpdir();
const envFile = join(tempDir, "test-export.env");
const envContent = `
export NODE_ENV=development
export PORT=3000
DEBUG=1
`;
writeFileSync(envFile, envContent);
try {
const result = process.loadEnvFile(envFile);
expect(result).toEqual({
NODE_ENV: "development",
PORT: "3000",
DEBUG: "1",
});
} finally {
unlinkSync(envFile);
}
});
it("should handle comments and empty lines", () => {
const tempDir = tmpdir();
const envFile = join(tempDir, "test-comments.env");
const envContent = `
# This is a comment
FOO=bar
# Another comment
BAZ=qux
`;
writeFileSync(envFile, envContent);
try {
const result = process.loadEnvFile(envFile);
expect(result).toEqual({
FOO: "bar",
BAZ: "qux",
});
} finally {
unlinkSync(envFile);
}
});
it("should throw an error for non-existent files", () => {
expect(() => {
process.loadEnvFile("/non/existent/file.env");
}).toThrow();
});
it("should throw an error when no path is provided", () => {
expect(() => {
// @ts-ignore
process.loadEnvFile();
}).toThrow();
});
it("should throw an error when path is not a string", () => {
expect(() => {
// @ts-ignore
process.loadEnvFile(123);
}).toThrow();
});
});