diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eadd404 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +.vscode +test.ts \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b23e82a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 cross-org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..495666e --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +## **Flexible Environment Variable Management for Deno, Bun, and Node.js** + +This library provides a consistent and simple interface for managing environment variables across multiple runtimes, +making it ideal for cross-platform development. + +## **Features** + +- **Cross-runtime support:** Works seamlessly within Deno, Bun, and Node.js environments. +- **Get and Set environment variables:** Retrieve and Modify environment variables in a consistent interface across + multiple runtimes. +- **Validation:** Ensures environment variables are valid before usage. +- **Error handling:** Provides clear error messages for unsupported runtimes or validation failures. +- **Optional environmental file loading:** Supports loading variables from custom .env files _(experimental)_ + +## **Installation** + +```bash +#For Deno +deno add @cross/env + +#For Bun +bunx jsr add @cross/env + +#For Node.js +npx jsr add @cross/env +``` + +## Getting Started + +**Usage Examples** + +import relevant functions. + +```javascript +import { getEnv, setEnv, validateEnv } from "@cross/env"; +``` + +Simple get example. + +```javascript +const apiKey = getEnv("API_KEY"); + +// or +console.log(`Home directory: ${getEnv("HOME")}`); +``` + +Simple set example. + +```javascript +setEnv("ENVIRONMENT", "development"); +setEnv("THE_COLOUR", "red"); +``` + +Checking if a variable exists. + +```javascript +if (hasEnv("DB_USER")) { + // Handle database connection logic +} +``` + +Getting all environment variables. + +```javascript +// getting all variables +const allVariables = getAllEnv(); +// getting all variables prefixed with API_ +const apiVariables = getAllEnv("API_"); +// Output: +// { API_KEY: 'abc123', API_VERSION: 'v2' } +``` + +Validation through custom functions. + +```javascript +// Validate a colour and execute conditional code. +const colourTest: ValidatorFunction = (value) => value === "red" || value === "green"; +if (validateEnv("THE_COLOUR", colourTest)) { + console.log("Yep, its red or green."); +} +``` + +Validation through custom functions and getting the variable content. + +```javascript +// or validating and getting a port number. +const isValidPort = (value: string): boolean => /^\d+$/.test(value); +const port = validateAndGetEnv("PORT", isValidPort); + +// or checking it we are reading a positive number. +function isPositiveNumber(value: string): boolean { + return !isNaN(Number(value)) && Number(value) > 0; +} +const timeout = validateAndGetEnv("TIMEOUT", isPositiveNumber); +``` + +## **Configuration (optional):** + +For more advanced use cases you can configure the behaviour of the library. The library defaults to showing console +warnings but not throwing errors. + +```javascript +await setupEnv({ + throwErrors: true, // Throw errors in unsupported runtimes + logWarnings: false, // Disable warnings + loadDotEnv: true, // Load from a .env file (experimental) + dotEnvFile: ".env.local", // Specify an alternate .env file (experimental) +}); +``` + +**Experimental .env File Support** + +Use the `loadDotEnv` parameter and optionally `dotEnvFile` in `setupEnv()` to automatically load environment variables +from a .env file. Currently, this feature might have runtime-specific limitations. + +## Issues + +Issues or questions concerning the library can be raised at the +[github repository](https://github.com/cross-org/env/issues) page. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..eaff856 --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,9 @@ +{ + "tasks": { + "publish": "deno publish --config jsr.json" + }, + "fmt": { + "lineWidth": 120, + "indentWidth": 4 + } +} diff --git a/jsr.jsonc b/jsr.jsonc new file mode 100644 index 0000000..338c715 --- /dev/null +++ b/jsr.jsonc @@ -0,0 +1,5 @@ +{ + "name": "@cross/env", + "version": "0.1.0", + "exports": "./mod.ts" +} diff --git a/lib/filehandler.ts b/lib/filehandler.ts new file mode 100644 index 0000000..beb5409 --- /dev/null +++ b/lib/filehandler.ts @@ -0,0 +1,115 @@ +import { FileReadError, Runtimes, UnsupportedEnvironmentError } from "./helpers.ts"; + +//Simulates/shims the Deno runtime for development purposes. +declare const Deno: { + readTextFileSync(filePath: string): string; + env: { + get(key: string): string | undefined; + set(key: string, value: string): void; + toObject(): Record; + }; +}; + +//Simulates/shims the Bun runtime for development purposes. +declare const Bun: { + file(filePath: string): { text(): string }; + env: Record; +}; + +//Simulates/shims Node.js function to load modules for development purposes. +// deno-lint-ignore no-explicit-any +declare const require: (module: string) => any; + +//Simulates/shims Node.js process object for development purposes. +declare const process: { env: Record }; + +//Simulates/shims the Node.js fs namespace for development purposes. +// deno-lint-ignore no-explicit-any +declare const fs: any; + +/** + * Loads environment variables from a .env file, handling file existence, + * runtime differences, and errors. + * + * @param {Runtimes} currentRuntime - The current runtime environment. + * @param {string} [filePath=".env"] - The path to the file to load, defaults to .env + * @param {boolean} throwErrors - Controls whether errors are thrown + * @param {boolean} logWarnings - Controls whether warnings are logged. + * @returns {Record} A object of parsed environment variables. + * @throws {UnsupportedEnvironmentError} If the runtime is unsupported and the 'throwErrors' flag is set. + * @throws {FileReadError} If there's an error reading the .env file and the 'throwErrors' flag is set. + */ +export async function loadEnvFile( + currentRuntime: Runtimes, + filePath: string = ".env", + throwErrors: boolean, + logWarnings: boolean, +): Promise> { + let fileContent = ""; + + try { + switch (currentRuntime) { + case Runtimes.Deno: + fileContent = Deno.readTextFileSync(filePath); + break; + case Runtimes.Bun: + fileContent = await Bun.file(filePath).text(); + break; + case Runtimes.Node: { + if (typeof fs === "undefined") { + const fs = require("fs"); + fileContent = fs.readFileSync(filePath, "utf-8"); + } else { + throw new Error("Node.js 'fs' module is not available in this environment."); + } + break; + } + default: + { + if (throwErrors) { + throw new UnsupportedEnvironmentError(); + } + if (logWarnings) { + console.warn("Unsupported runtime"); + } + } + break; + } + } catch (err) { + if (throwErrors) { + throw new FileReadError(err.message); + } + if (logWarnings) { + console.warn(err.message); + } + } + + return parseEnvFile(fileContent); +} + +/** + * Parses a string representing the content of a .env file and creates a + * dictionary of environment variables. + * + * @param {string} content - The string content of the .env file. + * @returns {Record} A object of parsed environment variables. + */ +function parseEnvFile(content: string): Record { + const envVars: Record = {}; + + if (content.length > 0) { + content.split("\n").forEach((line) => { + const trimmedLine = line.trim(); + + // Ignore comments and empty lines + if (!trimmedLine || trimmedLine.startsWith("#")) { + return; + } + + const [key, value] = trimmedLine.split("="); + envVars[key] = value; + }); + } + + return envVars; +} diff --git a/lib/helpers.ts b/lib/helpers.ts new file mode 100644 index 0000000..33eb3c2 --- /dev/null +++ b/lib/helpers.ts @@ -0,0 +1,57 @@ +/** + * Enum of supported runtimes. + */ +export enum Runtimes { + Deno = "deno", + Bun = "bun", + Node = "node", + Unsupported = "unsupported", +} + +/** + * Type alias for a validator function used in environment variable checks. + */ +export type ValidatorFunction = (value: string) => boolean; + +/** Env setup options. */ +export interface EnvOptions { + /** (default: false) - If true, throws an errors in unsupported runtimes. */ + throwErrors?: boolean; + /** (default: true) - If true, logs a warning to the console when environment variables + * are accessed in unsupported runtimes. */ + logWarnings?: boolean; + /** (default: false) - If true, read and load environment variables a file. + * (default file: ".env") **@experimental** Support for loading .env files may have + * limitations in certain runtimes. */ + loadDotEnv?: boolean; + /** (default: ".env") - filename of the file containing environment variables to load. */ + dotEnvFile?: string; +} + +/** + * Error thrown when attempting to set or retrieve environment variables in + * unsupported runtimes. + */ +export class UnsupportedEnvironmentError extends Error { + constructor() { + super("Unsupported runtime environment."); + } +} + +/** + * Error thrown when attempting to validate an environment variable. + */ +export class ValidationError extends Error { + constructor(message: string) { + super(message); + } +} + +/** + * Error thrown when attempting to read file with environment variables. + */ +export class FileReadError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..96716a4 --- /dev/null +++ b/mod.ts @@ -0,0 +1,263 @@ +/** + * @fileoverview A cross-runtime environment variable management library. + * Provides functions for getting, setting, validating, and + * retrieving environment variables across Deno, Bun and Node.js + */ + +import { + EnvOptions, + Runtimes, + UnsupportedEnvironmentError, + ValidationError, + ValidatorFunction, +} from "./lib/helpers.ts"; +import { loadEnvFile } from "./lib/filehandler.ts"; +export type { ValidatorFunction } from "./lib/helpers.ts"; + +/** + * Various shims/type-stubs, declared for development/IDE purposes + */ +//shims the Deno runtime +declare const Deno: { + env: { + get(key: string): string | undefined; + set(key: string, value: string): void; + toObject(): Record; + }; +}; +//shims the Bun runtime +declare const Bun: { env: Record }; +//shims Node.js function to load modules +// deno-lint-ignore no-explicit-any +declare const require: (module: string) => any; +//shims Node.js process object +declare const process: { env: Record }; + +// Flags to control behavior (initialized with defaults) +let throwErrors = false; +let logWarnings = true; + +function getCurrentRuntime(): Runtimes { + if (typeof Deno === "object") { + return Runtimes.Deno; + } else if (typeof Bun === "object") { + return Runtimes.Bun; + } else if (typeof process === "object" && typeof require === "function") { + return Runtimes.Node; + } else { + return Runtimes.Unsupported; + } +} + +/** + * Configures the behavior of the environment variable library. + * + * @param {EnvOptions} options - setup options. + */ +export async function setupEnv(options?: EnvOptions) { + if (options) { + throwErrors = options.throwErrors ?? false; + logWarnings = options.logWarnings ?? true; + + if (options.loadDotEnv) { + const currentRuntime = getCurrentRuntime(); + const envFile = options.dotEnvFile ? options.dotEnvFile : undefined; + const envVars = await loadEnvFile(currentRuntime, envFile, throwErrors, logWarnings); + + switch (currentRuntime) { + case Runtimes.Deno: + Object.entries(envVars).forEach(([key, value]) => Deno.env.set(key, value)); + break; + case Runtimes.Bun: + Object.entries(envVars).forEach(([key, value]) => Bun.env[key] = value); + break; + case Runtimes.Node: + Object.entries(envVars).forEach(([key, value]) => process.env[key] = value); + break; + } + } + } +} + +/** + * Gets an environment variable across different supported runtimes. + * + * @param {string} key - The name of the environment variable. + * @returns {string | undefined} The value of the environment variable, or undefined if not found. + * @throws {UnsupportedEnvironmentError} if the current runtime is unsupported + * and the 'throwErrors' flag is set. + */ +export function getEnv(key: string): string | undefined { + const currentRuntime = getCurrentRuntime(); + + switch (currentRuntime) { + case Runtimes.Deno: + return Deno.env.get(key); + case Runtimes.Bun: + return Bun.env[key]; + case Runtimes.Node: + return process.env[key]; + default: + if (throwErrors) { + throw new UnsupportedEnvironmentError(); + } + if (logWarnings) { + console.warn("Unsupported runtime"); + } + return undefined; + } +} + +/** + * Set an environment variable in supported runtimes. + * + * @param {string} key - The name of the environment variable. + * @param {string} value - The value to set for the environment variable. + * @throws {UnsupportedEnvironmentError} if the current runtime is unsupported + * and the 'throwErrors' flag is set. + */ +export function setEnv(key: string, value: string): void { + const currentRuntime = getCurrentRuntime(); + + switch (currentRuntime) { + case Runtimes.Deno: + Deno.env.set(key, value); + break; + case Runtimes.Bun: + Bun.env[key] = value; + break; + case Runtimes.Node: + process.env[key] = value; + break; + default: + if (throwErrors) { + throw new UnsupportedEnvironmentError(); + } + if (logWarnings) { + console.warn("Unsupported runtime"); + } + } +} + +/** + * Checks if an environment variable with the given key exists in the + * current runtime + * + * @param {string} key - The name of the environment variable. + * @returns {boolean} True if the environment variable exists, false otherwise. + * @throws {UnsupportedEnvironmentError} if the current runtime is unsupported + * and the 'throwErrors' flag is set. + */ +export function hasEnv(key: string): boolean { + const currentRuntime = getCurrentRuntime(); + + switch (currentRuntime) { + case Runtimes.Deno: + return Deno.env.get(key) !== undefined; + case Runtimes.Bun: + return key in Bun.env; + case Runtimes.Node: + return process.env[key] !== undefined; + default: + if (throwErrors) { + throw new UnsupportedEnvironmentError(); + } + if (logWarnings) { + console.warn("Unsupported runtime"); + } + return false; // Unsupported runtime + } +} + +/** + * Returns an object containing the accessible environment variables in the + * current runtime, optionally filtered by a prefix. + * + * @param {string} prefix - Optional prefix to filter environment variables by. + * @returns {Record} An object where keys are environment variable names and values + * are their corresponding values (or undefined if not found). + * @throws {UnsupportedEnvironmentError} if the current runtime is unsupported + * and the 'throwErrors' flag is set. + */ +export function getAllEnv(prefix?: string): Record { + const currentRuntime = getCurrentRuntime(); + const envVars: Record = {}; + + switch (currentRuntime) { + case Runtimes.Deno: + for (const key of Object.keys(Deno.env.toObject())) { + if (!prefix || key.startsWith(prefix)) { + envVars[key] = Deno.env.get(key); + } + } + break; + case Runtimes.Bun: + for (const key in Bun.env) { + if (!prefix || key.startsWith(prefix)) { + envVars[key] = Bun.env[key]; + } + } + break; + case Runtimes.Node: + for (const key in process.env) { + if (!prefix || key.startsWith(prefix)) { + envVars[key] = process.env[key]; + } + } + break; + default: + if (throwErrors) { + throw new UnsupportedEnvironmentError(); + } + if (logWarnings) { + console.warn("Unsupported runtime"); + } + } + + return envVars; +} + +/** + * Checks if an environment variable exists and validates it against a + * provided validation function. + * + * @param {string} key - The name of the environment variable. + * @param {ValidatorFunction} validator - A function that takes a string value and returns a boolean + * indicating whether the value is valid. + * @returns {boolean} True if the environment variable exists and passes validation, false otherwise. + */ +export function validateEnv(key: string, validator: ValidatorFunction): boolean { + const value = getEnv(key); + + if (value !== undefined && validator(value)) { + return true; + } else { + return false; + } +} + +/** + * Gets an environment variable across different supported runtimes, + * validating it against a provided validation function. + * + * @param {string} key - The name of the environment variable. + * @param {ValidatorFunction} validator - A function that takes a string value and returns a boolean + * indicating whether the value is valid. + * @returns The value of the environment variable if it exists and is valid. + * @throws ValidationError if the environment variable is found but fails validation. + */ +export function validateAndGetEnv(key: string, validator: ValidatorFunction): string | undefined { + const value = getEnv(key) || undefined; + + if (value && !validator(value)) { + if (throwErrors) { + throw new ValidationError(`Environment variable '${key}' is invalid.`); + } + if (logWarnings) { + console.warn(`Environment variable '${key}' is invalid.`); + } + return undefined; + } + + return value; +}