From 0ec6345f868e9f104dd59261f5f1718bcaf74628 Mon Sep 17 00:00:00 2001 From: olebhansen <951969+olebhansen@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:45:15 +0200 Subject: [PATCH] feat: support env specific vars --- .../ReduxStore/slices/collections/actions.js | 1 + packages/bruno-cli/src/commands/run.js | 9 +++++- packages/bruno-electron/src/app/watcher.js | 6 ++-- packages/bruno-electron/src/ipc/collection.js | 14 +++++++- .../bruno-electron/src/store/process-env.js | 32 +++++++++++++------ packages/bruno-lang/v2/src/dotenvToJson.js | 19 ++++++++++- .../bruno-lang/v2/tests/dotenvToJson.spec.js | 30 +++++++++++++---- readme.md | 12 +++++++ 8 files changed, 101 insertions(+), 22 deletions(-) diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index bbbc9a61e1..a1c720b32f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -1059,6 +1059,7 @@ export const selectEnvironment = (environmentUid, collectionUid) => (dispatch, g const { ipcRenderer } = window; ipcRenderer.invoke('renderer:update-ui-state-snapshot', { type: 'COLLECTION_ENVIRONMENT', data: { collectionPath: collection?.pathname, environmentName }}); + ipcRenderer.invoke('renderer:select-environment', { collectionUid, envName: environmentName }); dispatch(_selectEnvironment({ environmentUid, collectionUid })); resolve(); diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 3d4bfb6c70..a92907aef6 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -423,7 +423,14 @@ const handler = async function (argv) { const content = fs.readFileSync(dotEnvPath, 'utf8'); const jsonData = dotenvToJson(content); - forOwn(jsonData, (value, key) => { + const envName = envVars.__name__ || null; + const defaults = jsonData.default || {}; + const envSpecific = envName && jsonData.envs ? jsonData.envs[envName] || {} : {}; + + forOwn(defaults, (value, key) => { + processEnvVars[key] = value; + }); + forOwn(envSpecific, (value, key) => { processEnvVars[key] = value; }); } diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index 05fe9ebe87..aa6140d1f4 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -9,7 +9,7 @@ const { dotenvToJson } = require('@usebruno/lang'); const { uuid } = require('../utils/common'); const { getRequestUid } = require('../cache/requestUids'); const { decryptString } = require('../utils/encryption'); -const { setDotEnvVars } = require('../store/process-env'); +const { setDotEnvVars, getProcessEnvVars } = require('../store/process-env'); const { setBrunoConfig } = require('../store/bruno-config'); const EnvironmentSecretsStore = require('../store/env-secrets'); const UiStateSnapshot = require('../store/ui-state-snapshot'); @@ -183,7 +183,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread const payload = { collectionUid, processEnvVariables: { - ...jsonData + ...getProcessEnvVars(collectionUid) } }; win.webContents.send('main:process-env-update', payload); @@ -376,7 +376,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => { const payload = { collectionUid, processEnvVariables: { - ...jsonData + ...getProcessEnvVars(collectionUid) } }; win.webContents.send('main:process-env-update', payload); diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index b0924bb6b7..638506f727 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -39,7 +39,7 @@ const CollectionSecurityStore = require('../store/collection-security'); const UiStateSnapshotStore = require('../store/ui-state-snapshot'); const interpolateVars = require('./network/interpolate-vars'); const { getEnvVars, getTreePathFromCollectionToItem, mergeVars, parseBruFileMeta, hydrateRequestWithUuid, transformRequestToSaveToFilesystem } = require('../utils/collection'); -const { getProcessEnvVars } = require('../store/process-env'); +const { getProcessEnvVars, setActiveEnv } = require('../store/process-env'); const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, refreshOauth2Token } = require('../utils/oauth2'); const { getCertsAndProxyConfig } = require('./network'); @@ -958,6 +958,18 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }); + ipcMain.handle('renderer:select-environment', async (event, { collectionUid, envName }) => { + try { + setActiveEnv(collectionUid, envName); + mainWindow.webContents.send('main:process-env-update', { + collectionUid, + processEnvVariables: { ...getProcessEnvVars(collectionUid) } + }); + } catch (error) { + return Promise.reject(error); + } + }); + ipcMain.handle('renderer:fetch-oauth2-credentials', async (event, { itemUid, request, collection }) => { try { if (request.oauth2) { diff --git a/packages/bruno-electron/src/store/process-env.js b/packages/bruno-electron/src/store/process-env.js index 084187d2d6..f3ea9e4ec5 100644 --- a/packages/bruno-electron/src/store/process-env.js +++ b/packages/bruno-electron/src/store/process-env.js @@ -13,25 +13,39 @@ const dotEnvVars = {}; // collectionUid is a hash based on the collection path const getProcessEnvVars = (collectionUid) => { - // if there are no .env vars for this collection, return the process.env - if (!dotEnvVars[collectionUid]) { - return { - ...process.env - }; + const data = dotEnvVars[collectionUid]; + if (!data) { + return { ...process.env }; } - // if there are .env vars for this collection, return the process.env merged with the .env vars + const defaults = data.default || {}; + const envName = data.activeEnv; + const envSpecific = envName && data.envs ? data.envs[envName] || {} : {}; + return { ...process.env, - ...dotEnvVars[collectionUid] + ...defaults, + ...envSpecific }; }; const setDotEnvVars = (collectionUid, envVars) => { - dotEnvVars[collectionUid] = envVars; + if (!dotEnvVars[collectionUid]) { + dotEnvVars[collectionUid] = { activeEnv: null }; + } + dotEnvVars[collectionUid].default = envVars.default || {}; + dotEnvVars[collectionUid].envs = envVars.envs || {}; +}; + +const setActiveEnv = (collectionUid, envName) => { + if (!dotEnvVars[collectionUid]) { + dotEnvVars[collectionUid] = { default: {}, envs: {} }; + } + dotEnvVars[collectionUid].activeEnv = envName; }; module.exports = { getProcessEnvVars, - setDotEnvVars + setDotEnvVars, + setActiveEnv }; diff --git a/packages/bruno-lang/v2/src/dotenvToJson.js b/packages/bruno-lang/v2/src/dotenvToJson.js index 2c1794ee4c..6b24fa30db 100644 --- a/packages/bruno-lang/v2/src/dotenvToJson.js +++ b/packages/bruno-lang/v2/src/dotenvToJson.js @@ -3,7 +3,24 @@ const dotenv = require('dotenv'); const parser = (input) => { const buf = Buffer.from(input); const parsed = dotenv.parse(buf); - return parsed; + + const result = { default: {}, envs: {} }; + + Object.entries(parsed).forEach(([key, value]) => { + const match = key.match(/^(.*?)\.(.+)$/); + if (match) { + const env = match[1]; + const varName = match[2]; + if (!result.envs[env]) { + result.envs[env] = {}; + } + result.envs[env][varName] = value; + } else { + result.default[key] = value; + } + }); + + return result; }; module.exports = parser; diff --git a/packages/bruno-lang/v2/tests/dotenvToJson.spec.js b/packages/bruno-lang/v2/tests/dotenvToJson.spec.js index 4afa55647c..f75f8dfb27 100644 --- a/packages/bruno-lang/v2/tests/dotenvToJson.spec.js +++ b/packages/bruno-lang/v2/tests/dotenvToJson.spec.js @@ -3,7 +3,7 @@ const parser = require('../src/dotenvToJson'); describe('DotEnv File Parser', () => { test('it should parse a simple key-value pair', () => { const input = `FOO=bar`; - const expected = { FOO: 'bar' }; + const expected = { default: { FOO: 'bar' }, envs: {} }; const output = parser(input); expect(output).toEqual(expected); }); @@ -13,7 +13,7 @@ describe('DotEnv File Parser', () => { FOO=bar `; - const expected = { FOO: 'bar' }; + const expected = { default: { FOO: 'bar' }, envs: {} }; const output = parser(input); expect(output).toEqual(expected); }); @@ -25,9 +25,12 @@ BAZ=2 BEEP=false `; const expected = { - FOO: 'bar', - BAZ: '2', - BEEP: 'false' + default: { + FOO: 'bar', + BAZ: '2', + BEEP: 'false' + }, + envs: {} }; const output = parser(input); expect(output).toEqual(expected); @@ -37,7 +40,7 @@ BEEP=false const input = ` SPACE=" value " `; - const expected = { SPACE: ' value ' }; + const expected = { default: { SPACE: ' value ' }, envs: {} }; const output = parser(input); expect(output).toEqual(expected); }); @@ -46,7 +49,20 @@ SPACE=" value " const input = ` SPACE= value `; - const expected = { SPACE: 'value' }; + const expected = { default: { SPACE: 'value' }, envs: {} }; + const output = parser(input); + expect(output).toEqual(expected); + }); + + test('it should parse environment specific variables', () => { + const input = `dev.API_URL=http://dev\nprod.API_URL=http://prod`; + const expected = { + default: {}, + envs: { + dev: { API_URL: 'http://dev' }, + prod: { API_URL: 'http://prod' } + } + }; const output = parser(input); expect(output).toEqual(expected); }); diff --git a/readme.md b/readme.md index a63f019723..37b95d913b 100644 --- a/readme.md +++ b/readme.md @@ -138,6 +138,18 @@ If Bruno has helped you at work and your teams, please don't forget to share you Please see [here](publishing.md) for more information. +## .env Files + +You can define variables in a `.env` file at the root of your collection. Keys prefixed with `.` will only apply when that environment is selected. For example: + +```env +API_KEY=default +dev.API_KEY=dev-key +prod.API_KEY=prod-key +``` + +When the **dev** environment is active, `process.env.API_KEY` will resolve to `dev-key`. Without any environment selected, the default value will be used. + ## Stay in touch 🌐 [𝕏 (Twitter)](https://twitter.com/use_bruno)