UNPKG

dotenv-mono

Version:

This package permit to have a centralized dotenv on a monorepo. It also includes some extra features such as manipulation and saving of changes to the dotenv file, a default centralized file, and a file loader with ordering and priorities.

1,087 lines (1,084 loc) 52.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const index_1 = __importStar(require("./index")); const node_cli_1 = require("./node-cli"); const mock_fs_1 = __importDefault(require("mock-fs")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); describe("Dotenv Mono", () => { let instance; const originalEnv = process.env; const mockEnv = { root: "TEST_ROOT_ENV=1", overwrite: "TEST_OVERWRITE_ENV=1", malformed: "TEST_MALFORMED_ENV", malformedWithEol: "TEST_MALFORMED_ENV\r\n", defaults: "TEST_DEFAULT_ENV=1", webTest: "TEST_WEB_ENV=1", parent: "PARENT_ENV=1", child: "CHILD_ENV=1", }; beforeEach(() => { process.env = Object.assign(Object.assign({}, originalEnv), { NODE_ENV: "test" }); (0, mock_fs_1.default)({ "/root": { ".env": mockEnv.root, ".env.empty": "", ".env.overwrite": mockEnv.overwrite, ".env.defaults": mockEnv.defaults, ".env.malformed": mockEnv.malformed, ".env.malformed.eol": mockEnv.malformedWithEol, "apps": { "web": { ".env.test": mockEnv.webTest, }, }, }, "/parent": { ".env": mockEnv.parent, "child": { ".env": mockEnv.child, }, }, }); jest.spyOn(process, "cwd").mockReturnValue("/root/apps"); instance = new index_1.default({ defaults: ".env.defaults", encoding: "utf8", expand: true, override: false, }); }); afterEach(() => { // Restore original process.env process.env = originalEnv; jest.resetModules(); mock_fs_1.default.restore(); jest.restoreAllMocks(); }); it("should have config options", () => { expect(instance.cwd).toBeDefined(); expect(instance.debug).toBeDefined(); expect(instance.defaults).toBeDefined(); expect(instance.depth).toBeDefined(); expect(instance.encoding).toBeDefined(); expect(instance.expand).toBeDefined(); expect(instance.extension).toBeDefined(); expect(instance.override).toBeDefined(); expect(instance.path).toBeDefined(); expect(instance.priorities).toBeDefined(); }); it("should have a method parse()", () => { expect(instance.parse).toBeDefined(); }); it("should parse a dotenv string", () => { const output = instance.parse("TEST_PARSE_1: 1\nTEST_PARSE_2: 2"); expect(output).toEqual({ "TEST_PARSE_1": "1", "TEST_PARSE_2": "2" }); }); it("should have a method load()", () => { expect(instance.load).toBeDefined(); }); it("should load the expected environment variables from web directory", () => { jest.spyOn(process, "cwd").mockReturnValue("/root/apps/web"); expect(() => instance.load()).not.toThrow(); const expected = { "TEST_WEB_ENV": "1", "TEST_DEFAULT_ENV": "1" }; expect(instance.plain).toEqual(mockEnv.webTest); expect(instance.env).toEqual(expected); expect(process.env).toEqual(expect.objectContaining(expected)); }); it("should load the expected environment variables from web directory on production", () => { process.env.NODE_ENV = undefined; jest.spyOn(process, "cwd").mockReturnValue("/root/apps/web"); expect(() => instance.load()).not.toThrow(); const expected = { "TEST_ROOT_ENV": "1", "TEST_DEFAULT_ENV": "1" }; expect(instance.plain).toEqual(mockEnv.root); expect(instance.env).toEqual(expected); expect(process.env).toEqual(expect.objectContaining(expected)); }); it("should load the expected environment variables from specified path", () => { instance.path = "/root/.env.overwrite"; expect(() => instance.load()).not.toThrow(); const expected = { "TEST_OVERWRITE_ENV": "1", "TEST_DEFAULT_ENV": "1" }; expect(instance.plain).toEqual(mockEnv.overwrite); expect(instance.env).toEqual(expected); expect(process.env).toEqual(expect.objectContaining(expected)); }); it("should load the expected environment variables specifying priorities", () => { instance.priorities = { ".env.overwrite": 100, }; expect(() => instance.load()).not.toThrow(); const expected = { "TEST_OVERWRITE_ENV": "1", "TEST_DEFAULT_ENV": "1" }; expect(instance.plain).toEqual(mockEnv.overwrite); expect(instance.env).toEqual(expected); expect(process.env).toEqual(expect.objectContaining(expected)); }); it("should load the expected environment variables specifying an extension", () => { instance.extension = "overwrite"; expect(() => instance.load()).not.toThrow(); const expected = { "TEST_OVERWRITE_ENV": "1", "TEST_DEFAULT_ENV": "1" }; expect(instance.plain).toEqual(mockEnv.overwrite); expect(instance.env).toEqual(expected); expect(process.env).toEqual(expect.objectContaining(expected)); }); it("should prefer the nearer of two .env files with the same priority", () => { jest.spyOn(process, "cwd").mockReturnValue("/parent/child"); expect(() => instance.load()).not.toThrow(); const expected = { "CHILD_ENV": "1" }; expect(instance.env).toEqual(expected); expect(process.env).toEqual(expect.objectContaining(expected)); }); it("should load returns an empty output when none dotenv been found", () => { jest.spyOn(process, "cwd").mockReturnValue("/empty"); instance.cwd = process.cwd(); expect(() => instance.load()).not.toThrow(); expect(instance.plain).toBeEmpty(); expect(instance.env).toBeEmptyObject(); }); it("should load returns an empty output when the current working directory is too deep to reach dotenv", () => { jest.spyOn(process, "cwd").mockReturnValue("/root/path/too/deep/"); instance.depth = 2; instance.cwd = process.cwd(); expect(() => instance.load()).not.toThrow(); expect(instance.plain).toBeEmpty(); expect(instance.env).toBeEmptyObject(); }); it("should load returns an empty output when the specified file dotenv not been found", () => { instance.path = "/wrong/.env"; jest.spyOn(process, "cwd").mockReturnValue("/wrong"); expect(() => instance.load()).not.toThrow(); expect(instance.plain).toBeEmpty(); expect(instance.env).toBeEmptyObject(); }); it("should have a method loadFile() and load file without change the process env", () => { expect(instance.loadFile).toBeDefined(); expect(() => instance.loadFile()).not.toThrow(); const expected = { "TEST_ROOT_ENV": "1", "TEST_DEFAULT_ENV": "1" }; expect(instance.plain).toEqual(mockEnv.root); expect(instance.env).toEqual(expected); expect(process.env).not.toEqual(expect.objectContaining(expected)); }); it("should have a method save()", () => { expect(instance.save).toBeDefined(); }); it("should save changes", () => { expect(() => instance.loadFile()).not.toThrow(); const changes = { "TEST_ROOT_ENV": "2", "TEST_CHANGES_1_ENV": "10", "TEST_CHANGES_2_ENV": "enjoy", "TEST_CHANGES_3_ENV": "'enjoy quotes'", }; expect(() => instance.save(changes)).not.toThrow(); expect(instance.plain).toIncludeMultiple(toStringArray(changes)); }); it("should save changes on empty dotenv", () => { instance.path = "/root/.env.empty"; const changes = { "TEST_CHANGES_ENV": "1", "TEST_ROOT_ENV": "2" }; expect(() => instance.loadFile()).not.toThrow(); expect(() => instance.save(changes)).not.toThrow(); expect(instance.plain).toIncludeMultiple(toStringArray(changes)); }); it("should save changes on malformed dotenv", () => { instance.path = "/root/.env.malformed"; const changes = { "TEST_CHANGES_ENV": "1", "TEST_ROOT_ENV": "2" }; expect(() => instance.loadFile()).not.toThrow(); expect(() => instance.save(changes)).not.toThrow(); expect(instance.plain).toIncludeMultiple(toStringArray(changes)); }); it("should save changes on malformed with ends eol dotenv", () => { instance.path = "/root/.env.malformed.eol"; const changes = { "TEST_CHANGES_ENV": "1" }; expect(() => instance.loadFile()).not.toThrow(); expect(() => instance.save(changes)).not.toThrow(); expect(instance.plain).toIncludeMultiple(toStringArray(changes)); }); it("should not save changes on not existing file", () => { jest.spyOn(process, "cwd").mockReturnValue("/not/"); instance.path = "/not/.exists"; const changes = { "TEST_CHANGES_ENV": "1" }; expect(() => instance.loadFile()).not.toThrow(); expect(() => instance.save(changes)).not.toThrow(); expect(instance.plain).toBeEmpty(); }); it("should handle custom matcher for finding dotenv files", () => { const customMatcher = (dotenv, cwd) => { const customPath = path_1.default.join(cwd, ".env.custom"); if (fs_1.default.existsSync(customPath)) { return { foundPath: cwd, foundDotenv: customPath }; } return { foundPath: null, foundDotenv: null }; }; (0, mock_fs_1.default)({ "/custom": { ".env.custom": "CUSTOM_ENV=1", }, }); jest.spyOn(process, "cwd").mockReturnValue("/custom"); instance.cwd = process.cwd(); const foundFile = instance.find(customMatcher); expect(foundFile).toEqual(expect.stringContaining(".env.custom")); expect(foundFile).toEqual(expect.stringContaining("custom")); }); it("should test parse method with Buffer input", () => { const buffer = Buffer.from("BUFFER_TEST=1\nBUFFER_TEST_2=2"); const output = instance.parse(buffer); expect(output).toEqual({ "BUFFER_TEST": "1", "BUFFER_TEST_2": "2" }); }); it("should handle NODE_ENV undefined when setting priorities", () => { const originalNodeEnv = process.env.NODE_ENV; delete process.env.NODE_ENV; instance.extension = "test"; const priorities = instance.priorities; expect(priorities[".env.test.development"]).toBeDefined(); expect(priorities[".env.test.development.local"]).toBeDefined(); expect(priorities[".env.test.local"]).toBeDefined(); expect(priorities[".env.test"]).toBeDefined(); // Restore original NODE_ENV if (originalNodeEnv !== undefined) { process.env.NODE_ENV = originalNodeEnv; } }); it("should handle extension with leading and trailing dots", () => { instance.extension = "..test.."; expect(instance.extension).toEqual("test"); }); it("should handle save with null value (should skip)", () => { expect(() => instance.loadFile()).not.toThrow(); const changes = { "TEST_ROOT_ENV": "2", "UNDEFINED_VALUE": undefined, }; expect(() => instance.save(changes)).not.toThrow(); expect(instance.plain).toInclude("TEST_ROOT_ENV=2"); expect(instance.plain).not.toInclude("UNDEFINED_VALUE"); }); it("should handle debug mode", () => { const debugInstance = new index_1.default({ debug: true }); expect(debugInstance.debug).toBe(true); debugInstance.debug = false; expect(debugInstance.debug).toBe(false); }); it("should handle expand mode", () => { (0, mock_fs_1.default)({ "/expand": { ".env": "BASE_VAR=base\nEXPANDED_VAR=${BASE_VAR}_expanded", }, }); jest.spyOn(process, "cwd").mockReturnValue("/expand"); const expandInstance = new index_1.default({ cwd: "/expand", expand: true }); expandInstance.load(); expect(expandInstance.env.BASE_VAR).toBe("base"); expect(expandInstance.env.EXPANDED_VAR).toBe("base_expanded"); // Test with expand disabled const noExpandInstance = new index_1.default({ cwd: "/expand", expand: false }); noExpandInstance.load(); expect(noExpandInstance.env.EXPANDED_VAR).toBe("${BASE_VAR}_expanded"); }); it("should handle override mode", () => { // Set an existing env var process.env.TEST_OVERRIDE = "original"; (0, mock_fs_1.default)({ "/override": { ".env": "TEST_OVERRIDE=overridden", }, }); jest.spyOn(process, "cwd").mockReturnValue("/override"); // Test without override const noOverrideInstance = new index_1.default({ cwd: "/override", override: false }); noOverrideInstance.load(); expect(process.env.TEST_OVERRIDE).toBe("original"); // Test with override const overrideInstance = new index_1.default({ cwd: "/override", override: true }); overrideInstance.load(); expect(process.env.TEST_OVERRIDE).toBe("overridden"); // Clean up delete process.env.TEST_OVERRIDE; }); it("should handle different encodings", () => { instance.encoding = "latin1"; expect(instance.encoding).toBe("latin1"); instance.encoding = "ascii"; expect(instance.encoding).toBe("ascii"); }); it("should find dotenv with custom priorities and different NODE_ENV values", () => { (0, mock_fs_1.default)({ "/priority-test": { ".env": "BASE_ENV=1", ".env.staging": "STAGING_ENV=1", ".env.custom": "CUSTOM_ENV=1", }, }); // Test with staging environment process.env.NODE_ENV = "staging"; jest.spyOn(process, "cwd").mockReturnValue("/priority-test"); const stagingInstance = new index_1.default({ cwd: "/priority-test" }); stagingInstance.load(); expect(stagingInstance.env.STAGING_ENV).toBe("1"); // Test with custom priorities const customPriorityInstance = new index_1.default({ cwd: "/priority-test", priorities: { ".env.custom": 100 }, }); customPriorityInstance.load(); expect(customPriorityInstance.env.CUSTOM_ENV).toBe("1"); }); it("should handle file system errors gracefully", () => { // Test with directory that doesn't exist jest.spyOn(process, "cwd").mockReturnValue("/nonexistent"); const nonExistentInstance = new index_1.default({ cwd: "/nonexistent" }); expect(() => nonExistentInstance.load()).not.toThrow(); expect(nonExistentInstance.env).toEqual({}); }); it("should handle save with empty content and special characters", () => { (0, mock_fs_1.default)({ "/special": { ".env": "", }, }); instance.path = "/special/.env"; expect(() => instance.loadFile()).not.toThrow(); const changes = { "SPECIAL_CHARS": "value with spaces and 'quotes'", "EQUALS_IN_VALUE": "key=value", "NEWLINES": "line1\nline2", "CARRIAGE_RETURN": "line1\r\nline2", }; expect(() => instance.save(changes)).not.toThrow(); expect(instance.plain).toInclude("SPECIAL_CHARS=value with spaces and 'quotes'"); expect(instance.plain).toInclude("EQUALS_IN_VALUE=key=value"); expect(instance.plain).toInclude("NEWLINES=line1\\nline2"); expect(instance.plain).toInclude("CARRIAGE_RETURN=line1\\r\\nline2"); }); it("should expose dotenvLoad() function", () => { expect(index_1.dotenvLoad).toBeDefined(); }); it("should expose load() function", () => { expect(index_1.load).toBeDefined(); }); it("should dotenvLoad() return expected output", () => { const dotenv = (0, index_1.dotenvLoad)(); expect(dotenv.env).not.toBeEmptyObject(); }); it("should expose dotenvConfig() function", () => { expect(index_1.dotenvConfig).toBeDefined(); }); it("should expose config() function", () => { expect(index_1.config).toBeDefined(); }); it("should dotenvConfig() return expected output", () => { const output = (0, index_1.dotenvConfig)(); expect(output).toHaveProperty("parsed"); expect(output.parsed).not.toBeEmptyObject(); }); // Additional test cases for comprehensive coverage it("should handle find() method with no matcher and no files found", () => { jest.spyOn(process, "cwd").mockReturnValue("/nonexistent"); const emptyInstance = new index_1.default({ cwd: "/nonexistent" }); const result = emptyInstance.find(); expect(result).toBeNull(); }); it("should handle find() method when depth is reached without finding files", () => { jest.spyOn(process, "cwd").mockReturnValue("/root/very/deep/nested/path"); const deepInstance = new index_1.default({ cwd: "/root/very/deep/nested/path", depth: 2 }); const result = deepInstance.find(); expect(result).toBeNull(); }); it("should handle malformed .env files gracefully during parsing", () => { const malformedContent = "INVALID_FORMAT\n=MISSING_KEY\nVALID_KEY=value\n"; const parsed = instance.parse(malformedContent); expect(parsed).toHaveProperty("VALID_KEY", "value"); // Should ignore malformed lines and continue parsing }); it("should handle empty string input for parse method", () => { const parsed = instance.parse(""); expect(parsed).toEqual({}); }); it("should handle Buffer input with different encodings", () => { const content = "BUFFER_KEY=buffer_value"; const buffer = Buffer.from(content, "utf8"); const parsed = instance.parse(buffer); expect(parsed).toEqual({ "BUFFER_KEY": "buffer_value" }); }); it("should handle priority conflicts correctly (higher priority wins)", () => { (0, mock_fs_1.default)({ "/priority": { ".env": "BASE=base", ".env.local": "LOCAL=local", ".env.test": "TEST=test", ".env.test.local": "TEST_LOCAL=test_local", }, }); jest.spyOn(process, "cwd").mockReturnValue("/priority"); process.env.NODE_ENV = "test"; const priorityInstance = new index_1.default({ cwd: "/priority" }); priorityInstance.load(); // Should load .env.test.local (priority 75) over others expect(priorityInstance.env.TEST_LOCAL).toBe("test_local"); }); it("should handle directory traversal correctly when files exist at different levels", () => { (0, mock_fs_1.default)({ "/deep": { "level1": { "level2": { "level3": { // Empty directory }, }, ".env": "LEVEL2=level2", }, ".env": "ROOT=root", }, }); jest.spyOn(process, "cwd").mockReturnValue("/deep/level1/level2/level3"); const traversalInstance = new index_1.default({ cwd: "/deep/level1/level2/level3" }); traversalInstance.load(); // Should find .env at level2, not root (closer file wins) expect(traversalInstance.env.LEVEL2).toBe("level2"); expect(traversalInstance.env.ROOT).toBeUndefined(); }); it("should handle constructor with all undefined values", () => { const undefinedInstance = new index_1.default({ cwd: undefined, debug: undefined, defaults: undefined, depth: undefined, encoding: undefined, expand: undefined, extension: undefined, override: undefined, path: undefined, priorities: undefined, }); // Should use default values expect(undefinedInstance.cwd).toBe(process.cwd()); expect(undefinedInstance.debug).toBe(false); expect(undefinedInstance.defaults).toBe(".env.defaults"); expect(undefinedInstance.depth).toBe(4); expect(undefinedInstance.encoding).toBe("utf8"); expect(undefinedInstance.expand).toBe(true); expect(undefinedInstance.extension).toBe(""); expect(undefinedInstance.override).toBe(false); expect(undefinedInstance.path).toBe(""); }); it("should handle custom priorities with non-existent files", () => { const customInstance = new index_1.default({ cwd: "/root", priorities: { ".env.nonexistent": 100, ".env.another": 90, }, }); customInstance.load(); // Should fall back to existing files like .env expect(customInstance.env.TEST_ROOT_ENV).toBe("1"); }); it("should handle defaults file when no main dotenv file exists", () => { (0, mock_fs_1.default)({ "/defaults-only": { ".env.defaults": "DEFAULT_ONLY=value", }, }); jest.spyOn(process, "cwd").mockReturnValue("/defaults-only"); const defaultsInstance = new index_1.default({ cwd: "/defaults-only" }); defaultsInstance.load(); expect(defaultsInstance.env.DEFAULT_ONLY).toBe("value"); }); it("should handle save() with string numbers and special values", () => { expect(() => instance.loadFile()).not.toThrow(); const changes = { "STRING_NUMBER": "123", "BOOLEAN_STRING": "true", "EMPTY_STRING": "", "SPACES": " value with spaces ", "MULTILINE": "line1\\nline2", }; expect(() => instance.save(changes)).not.toThrow(); expect(instance.plain).toInclude("STRING_NUMBER=123"); expect(instance.plain).toInclude("BOOLEAN_STRING=true"); expect(instance.plain).toInclude("EMPTY_STRING="); expect(instance.plain).toInclude("SPACES=value with spaces"); expect(instance.plain).toInclude("MULTILINE=line1\\nline2"); }); it("should handle save() when file becomes invalid during operation", () => { // Test edge case where file might be deleted between loadFile and save instance.path = "/root/.env"; expect(() => instance.loadFile()).not.toThrow(); // Simulate file deletion (0, mock_fs_1.default)({}); const changes = { "NEW_VAR": "value" }; expect(() => instance.save(changes)).not.toThrow(); // Should handle gracefully without throwing }); it("should handle complex dotenv expansion scenarios", () => { (0, mock_fs_1.default)({ "/expansion": { ".env": "BASE=base\nNESTED=${BASE}_nested\nCHAINED=${NESTED}_chained\nSELF_REF=${SELF_REF}_loop", }, }); jest.spyOn(process, "cwd").mockReturnValue("/expansion"); const expandInstance = new index_1.default({ cwd: "/expansion", expand: true }); expandInstance.load(); expect(expandInstance.env.BASE).toBe("base"); expect(expandInstance.env.NESTED).toBe("base_nested"); expect(expandInstance.env.CHAINED).toBe("base_nested_chained"); // Should handle self-reference without infinite loop expect(expandInstance.env.SELF_REF).toContain("loop"); }); it("should handle extension normalization edge cases", () => { const testInstance = new index_1.default(); // Test various extension formats testInstance.extension = "...multiple.dots..."; expect(testInstance.extension).toBe("multiple.dots"); testInstance.extension = ".single.dot."; expect(testInstance.extension).toBe("single.dot"); testInstance.extension = "nodots"; expect(testInstance.extension).toBe("nodots"); testInstance.extension = ""; expect(testInstance.extension).toBe(""); }); it("should handle different NODE_ENV values in priorities generation", () => { const originalNodeEnv = process.env.NODE_ENV; // Test with staging environment process.env.NODE_ENV = "staging"; const stagingInstance = new index_1.default({ extension: "api" }); const stagingPriorities = stagingInstance.priorities; expect(stagingPriorities[".env.api.staging.local"]).toBe(75); expect(stagingPriorities[".env.api.local"]).toBe(50); expect(stagingPriorities[".env.api.staging"]).toBe(25); expect(stagingPriorities[".env.api"]).toBe(1); // Test with production environment process.env.NODE_ENV = "production"; const prodInstance = new index_1.default({ extension: "web" }); const prodPriorities = prodInstance.priorities; expect(prodPriorities[".env.web.production.local"]).toBe(75); expect(prodPriorities[".env.web.production"]).toBe(25); // Restore original NODE_ENV process.env.NODE_ENV = originalNodeEnv; }); it("should handle config property persistence across operations", () => { instance.load(); const initialConfig = Object.assign({}, instance.config); // Config should persist after loadFile instance.loadFile(); expect(instance.config.parsed).toEqual(initialConfig.parsed); // Config should be updated after save - ensure we have a valid file to save to instance.path = "/root/.env"; // Explicitly set a path to ensure save works const changes = { "NEW_CONFIG_VAR": "value" }; instance.save(changes); expect(instance.env).toHaveProperty("NEW_CONFIG_VAR", "value"); }); it("should handle file system permission errors gracefully", () => { // Test with directory that doesn't exist (this is safe and consistent) jest.spyOn(process, "cwd").mockReturnValue("/nonexistent-permission-test"); const permissionInstance = new index_1.default({ cwd: "/nonexistent-permission-test", path: "/nonexistent-permission-test/.env", }); expect(() => permissionInstance.load()).not.toThrow(); expect(permissionInstance.env).toEqual({}); }); it("should handle encoding edge cases", () => { const testInstance = new index_1.default(); // Test setting various encoding types testInstance.encoding = "ascii"; expect(testInstance.encoding).toBe("ascii"); testInstance.encoding = "base64"; expect(testInstance.encoding).toBe("base64"); testInstance.encoding = "hex"; expect(testInstance.encoding).toBe("hex"); // Test with undefined (should not change) const currentEncoding = testInstance.encoding; testInstance.encoding = undefined; expect(testInstance.encoding).toBe(currentEncoding); }); it("should handle dotenv matcher edge cases", () => { (0, mock_fs_1.default)({ "/matcher-test": { ".env": "BASE=base", "subdir": { ".env.local": "LOCAL=local", }, }, }); jest.spyOn(process, "cwd").mockReturnValue("/matcher-test"); // Test custom matcher that looks for files in subdirectories const customMatcher = (dotenv, cwd) => { const subdirPath = path_1.default.join(cwd, "subdir", ".env.local"); if (fs_1.default.existsSync(subdirPath)) { return { foundPath: cwd, foundDotenv: subdirPath }; } return { foundPath: null, foundDotenv: null }; }; const matcherInstance = new index_1.default({ cwd: "/matcher-test" }); const result = matcherInstance.find(customMatcher); expect(result).toEqual(expect.stringContaining(".env.local")); expect(result).toEqual(expect.stringContaining("matcher-test")); }); it("should handle save operations with existing content preservation", () => { (0, mock_fs_1.default)({ "/preserve": { ".env": "# Comment line\nEXISTING=value\n# Another comment\nOTHER=other\n", }, }); const preserveInstance = new index_1.default({ path: "/preserve/.env" }); preserveInstance.loadFile(); const changes = { "EXISTING": "updated_value", "NEW_VAR": "new_value", }; preserveInstance.save(changes); // Should preserve comments and update/add variables correctly expect(preserveInstance.plain).toInclude("# Comment line"); expect(preserveInstance.plain).toInclude("# Another comment"); expect(preserveInstance.plain).toInclude("EXISTING=updated_value"); expect(preserveInstance.plain).toInclude("NEW_VAR=new_value"); expect(preserveInstance.plain).toInclude("OTHER=other"); }); it("should handle file loading errors with debug mode", () => { // Create a mock instance with debug mode enabled const debugInstance = new index_1.default({ debug: true, cwd: "/root", path: "/root/.env" }); // Mock console.error to capture error output const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => { }); // Mock fs.readFileSync to throw an error const originalReadFileSync = fs_1.default.readFileSync; jest.spyOn(fs_1.default, "readFileSync").mockImplementationOnce(() => { throw new Error("Simulated file read error"); }); // This should trigger the error handling path in loadDotenv debugInstance.loadFile(); // Verify that console.error was called with debug mode on expect(consoleErrorSpy).toHaveBeenCalledWith("Error loading dotenv file: /root/.env", expect.any(Error)); // Restore mocks jest.spyOn(fs_1.default, "readFileSync").mockImplementation(originalReadFileSync); consoleErrorSpy.mockRestore(); }); // Edge cases for defaults not overriding main environment variables it("should not allow defaults to override main environment variables", () => { (0, mock_fs_1.default)({ "/defaults-test": { ".env": "SHARED_VAR=main_value\nMAIN_ONLY=main", ".env.defaults": "SHARED_VAR=default_value\nDEFAULT_ONLY=default", }, }); jest.spyOn(process, "cwd").mockReturnValue("/defaults-test"); const instance = new index_1.default({ cwd: "/defaults-test" }); instance.load(); expect(instance.env.SHARED_VAR).toBe("main_value"); // Main should win expect(instance.env.MAIN_ONLY).toBe("main"); expect(instance.env.DEFAULT_ONLY).toBe("default"); // Default should fill gaps }); // Edge cases for realistic monorepo structure it("should work in realistic monorepo structure", () => { (0, mock_fs_1.default)({ "/monorepo": { ".env": "ROOT_VAR=root", ".env.production": "PROD_VAR=prod", ".env.defaults": "DEFAULT_VAR=default", "packages": { "ui-lib": {}, "other-lib": {}, }, "apps": { "web": {}, "docs": { ".env.local": "DOCS_VAR=docs", }, }, }, }); // Test from apps/web - should find root .env process.env.NODE_ENV = "development"; jest.spyOn(process, "cwd").mockReturnValue("/monorepo/apps/web"); const webInstance = new index_1.default({ cwd: "/monorepo/apps/web" }); webInstance.load(); expect(webInstance.env.ROOT_VAR).toBe("root"); expect(webInstance.env.DEFAULT_VAR).toBe("default"); // Test from apps/docs - should find docs .env.local (higher priority) jest.spyOn(process, "cwd").mockReturnValue("/monorepo/apps/docs"); const docsInstance = new index_1.default({ cwd: "/monorepo/apps/docs" }); docsInstance.load(); expect(docsInstance.env.DOCS_VAR).toBe("docs"); expect(docsInstance.env.DEFAULT_VAR).toBe("default"); expect(docsInstance.env.ROOT_VAR).toBeUndefined(); // Should prefer closer file }); // Edge cases for priority conflicts with multiple files at different levels it("should handle priority conflicts correctly across directory levels", () => { (0, mock_fs_1.default)({ "/priority-levels": { ".env": "ROOT_BASE=root_base", ".env.test": "ROOT_TEST=root_test", "level1": { ".env": "L1_BASE=l1_base", "level2": { ".env.test.local": "L2_TEST_LOCAL=l2_test_local", }, }, }, }); process.env.NODE_ENV = "test"; jest.spyOn(process, "cwd").mockReturnValue("/priority-levels/level1/level2"); const instance = new index_1.default({ cwd: "/priority-levels/level1/level2" }); instance.load(); // Should find .env.test.local at level2 (priority 75, depth 0) // Over .env.test at root (priority 25, depth 2) // Over .env at level1 (priority 1, depth 1) expect(instance.env.L2_TEST_LOCAL).toBe("l2_test_local"); expect(instance.env.ROOT_TEST).toBeUndefined(); expect(instance.env.L1_BASE).toBeUndefined(); }); // Edge cases for extension with priorities it("should handle extension with complex priority scenarios", () => { (0, mock_fs_1.default)({ "/extension-priority": { ".env": "BASE=base", ".env.server": "SERVER=server", ".env.server.test": "SERVER_TEST=server_test", ".env.server.test.local": "SERVER_TEST_LOCAL=server_test_local", }, }); process.env.NODE_ENV = "test"; jest.spyOn(process, "cwd").mockReturnValue("/extension-priority"); const instance = new index_1.default({ cwd: "/extension-priority", extension: "server" }); instance.load(); // Should load .env.server.test.local (priority 75) expect(instance.env.SERVER_TEST_LOCAL).toBe("server_test_local"); expect(instance.env.SERVER_TEST).toBeUndefined(); expect(instance.env.SERVER).toBeUndefined(); expect(instance.env.BASE).toBeUndefined(); }); // Edge cases for depth limitations with priority files it("should respect depth limit even when higher priority files exist deeper", () => { (0, mock_fs_1.default)({ "/depth-test": { ".env.test.local": "DEEP_HIGH_PRIORITY=deep_high", "l1": { ".env": "L1_BASE=l1_base", "l2": { ".env": "L2_BASE=l2_base", "l3": { // Empty directory - start search from here }, }, }, }, }); process.env.NODE_ENV = "test"; jest.spyOn(process, "cwd").mockReturnValue("/depth-test/l1/l2/l3"); const instance = new index_1.default({ cwd: "/depth-test/l1/l2/l3", depth: 2 }); instance.load(); // Should find .env at l2 (depth 1 from l3) // Should NOT find .env.test.local at root (depth 3 from l3, beyond limit) expect(instance.env.L2_BASE).toBe("l2_base"); expect(instance.env.DEEP_HIGH_PRIORITY).toBeUndefined(); }); // Edge cases for malformed priority configuration it("should handle malformed priority configuration gracefully", () => { const instance = new index_1.default({ priorities: { ".env.malformed": -1, // Negative priority ".env.zero": 0, // Zero priority "": 100, // Empty filename ".env.valid": 50, }, }); // Should still work with valid priorities expect(instance.priorities[".env.valid"]).toBe(50); expect(instance.priorities[".env.malformed"]).toBe(-1); expect(instance.priorities[".env.zero"]).toBe(0); }); // Edge cases for override behavior with process.env it("should handle override behavior correctly with existing process.env", () => { process.env.EXISTING_PROCESS_VAR = "process_value"; process.env.SHARED_VAR = "process_shared"; (0, mock_fs_1.default)({ "/override-test": { ".env": "EXISTING_PROCESS_VAR=dotenv_value\nSHARED_VAR=dotenv_shared\nNEW_VAR=new", }, }); jest.spyOn(process, "cwd").mockReturnValue("/override-test"); // Test without override const noOverrideInstance = new index_1.default({ cwd: "/override-test", override: false }); noOverrideInstance.load(); expect(process.env.EXISTING_PROCESS_VAR).toBe("process_value"); expect(process.env.SHARED_VAR).toBe("process_shared"); expect(process.env.NEW_VAR).toBe("new"); // Test with override const overrideInstance = new index_1.default({ cwd: "/override-test", override: true }); overrideInstance.load(); expect(process.env.EXISTING_PROCESS_VAR).toBe("dotenv_value"); expect(process.env.SHARED_VAR).toBe("dotenv_shared"); // Clean up delete process.env.EXISTING_PROCESS_VAR; delete process.env.SHARED_VAR; delete process.env.NEW_VAR; }); // Edge cases for variable expansion with circular references it("should handle variable expansion edge cases", () => { (0, mock_fs_1.default)({ "/expansion-test": { ".env": "BASE_VAR=base\nEXPANDED_VAR=${BASE_VAR}_expanded\nCIRCULAR_A=${CIRCULAR_B}\nCIRCULAR_B=${CIRCULAR_A}\nUNDEFINED_REF=${NONEXISTENT}\nNESTED_VAR=${EXPANDED_VAR}_nested", }, }); jest.spyOn(process, "cwd").mockReturnValue("/expansion-test"); const expandInstance = new index_1.default({ cwd: "/expansion-test", expand: true }); expandInstance.load(); expect(expandInstance.env.BASE_VAR).toBe("base"); expect(expandInstance.env.EXPANDED_VAR).toBe("base_expanded"); expect(expandInstance.env.NESTED_VAR).toBe("base_expanded_nested"); // Circular references should be handled gracefully expect(expandInstance.env.CIRCULAR_A).toBeDefined(); expect(expandInstance.env.CIRCULAR_B).toBeDefined(); // Undefined references should remain as-is or be handled expect(expandInstance.env.UNDEFINED_REF).toBeDefined(); }); // Edge cases for file encoding issues it("should handle different encodings correctly", () => { const utf8Content = "UTF8_VAR=café"; const latin1Content = "LATIN1_VAR=caf\xe9"; // é in latin1 (0, mock_fs_1.default)({ "/encoding-test": { ".env.utf8": utf8Content, ".env.latin1": latin1Content, }, }); // Test UTF-8 const utf8Instance = new index_1.default({ path: "/encoding-test/.env.utf8", encoding: "utf8" }); utf8Instance.loadFile(); expect(utf8Instance.env.UTF8_VAR).toBe("café"); // Test Latin1 const latin1Instance = new index_1.default({ path: "/encoding-test/.env.latin1", encoding: "latin1" }); latin1Instance.loadFile(); expect(latin1Instance.env.LATIN1_VAR).toBeDefined(); }); // Edge cases for save with special line endings and formatting it("should handle save with special line endings and formatting", () => { (0, mock_fs_1.default)({ "/formatting-test": { ".env": "VAR1=value1\r\nVAR2=value2\nVAR3=value3\r\n", }, }); instance.path = "/formatting-test/.env"; instance.loadFile(); const changes = { "VAR1": "updated1", "NEW_VAR": "new", "MULTILINE": "line1\nline2\r\nline3", }; expect(() => instance.save(changes)).not.toThrow(); expect(instance.plain).toInclude("VAR1=updated1"); expect(instance.plain).toInclude("NEW_VAR=new"); expect(instance.plain).toInclude("MULTILINE=line1\\nline2\\r\\nline3"); }); // Edge cases for custom matchers it("should handle custom matcher edge cases", () => { const failingMatcher = () => { throw new Error("Matcher error"); }; const emptyMatcher = () => ({ foundPath: null, foundDotenv: null, }); (0, mock_fs_1.default)({ "/matcher-test": { ".env": "TEST=value", }, }); jest.spyOn(process, "cwd").mockReturnValue("/matcher-test"); const instance = new index_1.default({ cwd: "/matcher-test" }); // Test with failing matcher expect(() => instance.find(failingMatcher)).toThrow(); // Test with empty matcher const result = instance.find(emptyMatcher); expect(result).toBeNull(); }); // Edge cases for empty and whitespace-only files it("should handle empty and whitespace-only dotenv files", () => { (0, mock_fs_1.default)({ "/empty-files": { ".env.empty": "", ".env.whitespace": " \n \r\n \t \n", ".env.comments": "# Only comments\n# Another comment", }, }); jest.spyOn(process, "cwd").mockReturnValue("/empty-files"); // Test empty file const emptyInstance = new index_1.default({ path: "/empty-files/.env.empty" }); emptyInstance.loadFile(); expect(emptyInstance.env).toEqual({}); // Test whitespace-only file const whitespaceInstance = new index_1.default({ path: "/empty-files/.env.whitespace" }); whitespaceInstance.loadFile(); expect(whitespaceInstance.env).toEqual({}); // Test comments-only file const commentsInstance = new index_1.default({ path: "/empty-files/.env.comments" }); commentsInstance.loadFile(); expect(commentsInstance.env).toEqual({}); }); // Edge cases for file permissions and access errors it("should handle file access errors gracefully", () => { // Mock fs.existsSync to return true but readFileSync to fail const originalExistsSync = fs_1.default.existsSync; const originalReadFileSync = fs_1.default.readFileSync; jest.spyOn(fs_1.default, "existsSync").mockReturnValue(true); jest.spyOn(fs_1.default, "readFileSync").mockImplementation(() => { throw new Error("Permission denied"); }); const instance = new index_1.default({ path: "/inaccessible/.env" }); expect(() => instance.loadFile()).not.toThrow(); expect(instance.env).toEqual({}); // Restore mocks jest.spyOn(fs_1.default, "existsSync").mockImplementation(originalExistsSync); jest.spyOn(fs_1.default, "readFileSync").mockImplementation(originalReadFileSync); }); it("should handle CLI preload scenario simulation", () => { // Simulate the CLI preload behavior mentioned in README process.env.DOTENV_CONFIG_PATH = "/root/.env"; process.env.DOTENV_CONFIG_DEBUG = "true"; // Use the CLI functionality const dotenv = (0, node_cli_1.runNodeCli)(index_1.load); expect(dotenv.debug).toBe(true); expect(dotenv.path).toBe("/root/.env"); // Clean up delete process.env.DOTENV_CONFIG_PATH; delete process.env.DOTENV_CONFIG_DEBUG; }); it("should handle very deep directory structures beyond reasonable limits", () => { const deepPath = "/deep-root/" + "level/".repeat(20); // 20 levels deep (0, mock_fs_1.default)({ "/deep-root": { ".env": "DEEP_ROOT=root", [`level/${"level/".repeat(19)}`]: { // Very deep nested structure }, }, }); jest.spyOn(process, "cwd").mockReturnValue(deepPath); const deepInstance = new index_1.default({ cwd: deepPath, depth: 25 }); // Allow deep search deepInstance.load(); expect(deepInstance.env.DEEP_ROOT).toBe("root"); }); it("should handle simultaneous file access scenarios", () => { // Test concurrent access patterns that might occur in real applications (0, mock_fs_1.default)({ "/concurrent": { ".env": "CONCURRENT=value", ".env.defaults": "DEFAULT=default", }, }); jest.spyOn(process, "cwd").mockReturnValue("/concurrent"); // Create multiple instances that might access files simultaneously const instance1 = new index_1.default({ cwd: "/concurrent" }); const instance2 = new index_1.default({ cwd: "/concurrent" }); const instance3 = new index_1.default({ cwd: "/concurrent" }); // All should succeed without interference expect(() => { instance1.load(); instance2.loadFile(); instance3.load(); }).not.toThrow(); expect(instance1.env.CONCURRENT).toBe("value"); expect(instance2.env.CONCURRENT).toBe("value"); expect(instance3.env.CONCURRENT).toBe("value"); }); it("should handle binary data in dotenv files gracefully", () => { // Create a file with binary data that could cause issues const binaryContent = "NORMAL_VAR=value\nBINARY_VAR=\x00\x01\x02\xFF\nANOTHER_VAR=normal"; (0, mock_fs_1.default)({ "/binary-test": { ".env": Buffer.from(binaryContent, "binary"), }, }); const instance = new index_1.default({ path: "/binary-test/.env" }); expect(() => instance.loadFile()).not.toThrow(); // Should parse what it can expect(instance.env.NORMAL_VAR).toBe("value"); expect(instance.env.ANOTHER_VAR).toBe("normal"); }); it("should handle extremely large dotenv files", () => { // Create a large file with many variables const largeContent = Array.from({ length: 1000 }, (_, i) => `VAR_${i}=value_${i}`).join("\n"); (0, mock_fs_1.default)({ "/large-test": { ".env": largeContent, }, }); const instance = new index_1.default({ path: "/large-test/.env" }); expect(() => instance.loadFile()).not.toThrow(); // Should handle all variables expect(instance.env.VAR_0).toBe("value_0"); expect(instance.env.VAR_999).toBe("value_999"); expect(Object.keys(instance.env)).toHaveLength(1000); }); it("should handle dotenv files with only whitespace and comments", () => { const whitespaceOnlyContent = ` # This is a comment # Another comment with leading spaces # Tab-indented comment # Final comment `; (0, mock_fs_1.default)({ "/whitespace-test": { ".env": whitespaceOnlyContent, }, }); const instance = new index_1.default({ path: "/whitespace-test/.env" }); expect(() => instance.loadFile()).not.toThrow(); expect(instance.env).toEqual({}); expect(instance.plain).toBe(whitespaceOnlyContent); }); it("should handle edge cases in variable name validation", () => { const edgeCaseContent = ` NORMAL_VAR=value 123_STARTS_WITH_NUMBER=invalid_but_parsed _STARTS_WITH_UNDERSCORE=valid CONTAINS-DASH=valid CONTAINS.DOT=valid CONTAINS SPACE=invalid_but_might_be_parsed UNICODE_名前=unicode_name EMPTY_VALUE= EQUALS_IN_NAME=WITH=EQUALS=value `; (0, mock_fs_1.default)({ "/edge-vars": { ".env": edgeCaseContent, }, }); const instance = new index_1.default({ path: "/edge-vars/.env" }); expect(() => instance