From 95f52582035dd446c21293fdcd644ac407057566 Mon Sep 17 00:00:00 2001 From: Jorge Rinaldi Date: Sun, 18 Oct 2026 14:03:37 +0200 Subject: [PATCH] Add loadEnvFile --- CHANGELOG.md | 1 + src/Node/Process.js | 1 + src/Node/Process.purs | 16 ++++++++++++++ test/Main.purs | 50 +++++++++++++++++++++++++++++++++++++++---- test/fixtures/.env | 2 ++ 5 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/.env diff --git a/CHANGELOG.md b/CHANGELOG.md index a36d1dd..d5c6de6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Notable changes to this project are documented in this file. The format is based Breaking changes: New features: +- Add FFI for `process.loadEnvFile` as `loadEnvFile` and `loadEnvFile'` (#47 by @m-rinaldi) Bugfixes: diff --git a/src/Node/Process.js b/src/Node/Process.js index 3af2c4e..14558fb 100644 --- a/src/Node/Process.js +++ b/src/Node/Process.js @@ -24,6 +24,7 @@ export const setEnvImpl = (key, val) => { export const unsetEnvImpl = (key) => { delete process.env[key]; }; +export const loadEnvFileImpl = (path) => process.loadEnvFile(path); export const execArgv = () => process.execArgv.slice(); export const execPath = () => process.execPath; export const exit = () => process.exit(); diff --git a/src/Node/Process.purs b/src/Node/Process.purs index 1c6c46c..3d059de 100644 --- a/src/Node/Process.purs +++ b/src/Node/Process.purs @@ -32,6 +32,8 @@ module Node.Process , lookupEnv , setEnv , unsetEnv + , loadEnvFile + , loadEnvFile' , execArgv , execPath , exit @@ -332,6 +334,20 @@ unsetEnv key = runEffectFn1 unsetEnvImpl key foreign import unsetEnvImpl :: EffectFn1 (String) (Unit) +-- | Load the `.env` file in the current working directory into `process.env`. +-- | Variables already present in the environment are not overwritten. +-- | Throws if the file does not exist. Lines that cannot be parsed are skipped. +loadEnvFile :: Effect Unit +loadEnvFile = runEffectFn1 loadEnvFileImpl ".env" + +-- | Load the given `.env`-formatted file into `process.env`. +-- | Variables already present in the environment are not overwritten. +-- | Throws if the file does not exist. Lines that cannot be parsed are skipped. +loadEnvFile' :: String -> Effect Unit +loadEnvFile' path = runEffectFn1 loadEnvFileImpl path + +foreign import loadEnvFileImpl :: EffectFn1 String Unit + -- | Get a copy of the Node-specific options passed to the `node` executable. foreign import execArgv :: Effect (Array String) diff --git a/test/Main.purs b/test/Main.purs index 93d76a8..49f9b68 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -4,12 +4,17 @@ import Prelude import Data.Maybe (Maybe(..)) import Effect (Effect) -import Effect.Exception (throw) +import Effect.Exception (catchException, throw) import Foreign.Object as Object import Node.Process as Process main :: Effect Unit main = do + testEnvMutation + testLoadEnvFile + +testEnvMutation :: Effect Unit +testEnvMutation = do let key = "PURESCRIPT_NODE_PROCESS_TEST" original <- Process.lookupEnv key Process.setEnv key "first" @@ -18,11 +23,48 @@ main = do current <- Process.lookupEnv key Process.unsetEnv key removed <- Process.lookupEnv key - case original of - Nothing -> Process.unsetEnv key - Just value -> Process.setEnv key value + restoreEnv key original if Object.lookup key snapshot == Just "first" && current == Just "second" && removed == Nothing then pure unit else throw "Environment lookup, snapshot, or removal failed" + +testLoadEnvFile :: Effect Unit +testLoadEnvFile = do + let loadedKey = "PURESCRIPT_NODE_PROCESS_ENV_FILE_LOADED" + let existingKey = "PURESCRIPT_NODE_PROCESS_ENV_FILE_EXISTING" + originalLoaded <- Process.lookupEnv loadedKey + originalExisting <- Process.lookupEnv existingKey + originalCwd <- Process.cwd + + -- Explicit path: loads new variables, keeps existing ones. + Process.unsetEnv loadedKey + Process.setEnv existingKey "already-set" + Process.loadEnvFile' "test/fixtures/.env" + loadedByPath <- Process.lookupEnv loadedKey + kept <- Process.lookupEnv existingKey + + -- Default path: resolves `.env` against the current working directory. + Process.unsetEnv loadedKey + Process.chdir "test/fixtures" + Process.loadEnvFile + Process.chdir originalCwd + loadedByDefault <- Process.lookupEnv loadedKey + + -- Missing file throws. + missingThrew <- Process.loadEnvFile' "test/fixtures/missing.env" $> false # catchException \_ -> pure true + + restoreEnv loadedKey originalLoaded + restoreEnv existingKey originalExisting + if + loadedByPath == Just "from-file" + && kept == Just "already-set" + && loadedByDefault == Just "from-file" + && missingThrew then pure unit + else throw "loadEnvFile did not load the file, overwrote an existing variable, or did not throw for a missing file" + +restoreEnv :: String -> Maybe String -> Effect Unit +restoreEnv key = case _ of + Nothing -> Process.unsetEnv key + Just value -> Process.setEnv key value diff --git a/test/fixtures/.env b/test/fixtures/.env new file mode 100644 index 0000000..8849592 --- /dev/null +++ b/test/fixtures/.env @@ -0,0 +1,2 @@ +PURESCRIPT_NODE_PROCESS_ENV_FILE_LOADED=from-file +PURESCRIPT_NODE_PROCESS_ENV_FILE_EXISTING=should-not-win