mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 04:35:40 +00:00
feat: workspace .env file support (#6777)
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
collectionUnlinkEnvFileEvent,
|
||||
collectionUnlinkFileEvent,
|
||||
processEnvUpdateEvent,
|
||||
workspaceEnvUpdateEvent,
|
||||
requestCancelled,
|
||||
runFolderEvent,
|
||||
runRequestEvent,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUiStateSnapshot, mergeAndPersistEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { workspaceOpenedEvent, workspaceConfigUpdatedEvent } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { workspaceDotEnvUpdateEvent } from 'providers/ReduxStore/slices/workspaces';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
@@ -214,6 +216,11 @@ const useIpcEvents = () => {
|
||||
dispatch(processEnvUpdateEvent(val));
|
||||
});
|
||||
|
||||
const removeWorkspaceDotEnvUpdatesListener = ipcRenderer.on('main:workspace-dotenv-update', (val) => {
|
||||
dispatch(workspaceDotEnvUpdateEvent(val));
|
||||
dispatch(workspaceEnvUpdateEvent({ processEnvVariables: val.processEnvVariables }));
|
||||
});
|
||||
|
||||
const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => {
|
||||
console[val.type](...val.args);
|
||||
dispatch(addLog({
|
||||
@@ -293,6 +300,7 @@ const useIpcEvents = () => {
|
||||
removeRunFolderEventListener();
|
||||
removeRunRequestEventListener();
|
||||
removeProcessEnvUpdatesListener();
|
||||
removeWorkspaceDotEnvUpdatesListener();
|
||||
removeConsoleLogListener();
|
||||
removeConfigUpdatesListener();
|
||||
removeShowPreferencesListener();
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
sortCollections as _sortCollections,
|
||||
updateCollectionMountStatus,
|
||||
moveCollection,
|
||||
workspaceEnvUpdateEvent,
|
||||
requestCancelled,
|
||||
resetRunResults,
|
||||
responseReceived,
|
||||
@@ -2255,6 +2256,7 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid);
|
||||
const workspaceProcessEnvVariables = activeWorkspace?.processEnvVariables || {};
|
||||
|
||||
// Check if collection already exists in Redux state
|
||||
const existingCollection = state.collections.collections.find(
|
||||
@@ -2296,6 +2298,8 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(workspaceEnvUpdateEvent({ processEnvVariables: workspaceProcessEnvVariables }));
|
||||
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
@@ -2308,6 +2312,7 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
pathname: pathname,
|
||||
items: [],
|
||||
runtimeVariables: {},
|
||||
workspaceProcessEnvVariables,
|
||||
brunoConfig: brunoConfig
|
||||
};
|
||||
|
||||
@@ -2326,6 +2331,9 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
);
|
||||
|
||||
if (currentWorkspace) {
|
||||
// Set collection-workspace mapping for workspace env vars
|
||||
ipcRenderer.invoke('renderer:set-collection-workspace', uid, currentWorkspace.pathname);
|
||||
|
||||
const alreadyInWorkspace = currentWorkspace.collections?.some(
|
||||
(c) => normalizePath(c.path) === normalizePath(pathname)
|
||||
);
|
||||
|
||||
@@ -405,6 +405,12 @@ export const collectionsSlice = createSlice({
|
||||
collection.processEnvVariables = processEnvVariables;
|
||||
}
|
||||
},
|
||||
workspaceEnvUpdateEvent: (state, action) => {
|
||||
const { processEnvVariables } = action.payload;
|
||||
state.collections.forEach((collection) => {
|
||||
collection.workspaceProcessEnvVariables = processEnvVariables;
|
||||
});
|
||||
},
|
||||
requestCancelled: (state, action) => {
|
||||
const { itemUid, collectionUid, seq, timestamp } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -3424,6 +3430,7 @@ export const {
|
||||
cloneItem,
|
||||
scriptEnvironmentUpdateEvent,
|
||||
processEnvUpdateEvent,
|
||||
workspaceEnvUpdateEvent,
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
runGrpcRequestEvent,
|
||||
|
||||
@@ -169,8 +169,8 @@ export const removeCollectionFromWorkspaceAction = (workspaceUid, collectionPath
|
||||
};
|
||||
|
||||
const loadWorkspaceCollectionsForSwitch = async (dispatch, workspace) => {
|
||||
const openCollectionsFunction = (collectionPaths, workspaceId) => {
|
||||
return dispatch(openMultipleCollections(collectionPaths, { workspaceId }));
|
||||
const openCollectionsFunction = (collectionPaths, workspacePath) => {
|
||||
return dispatch(openMultipleCollections(collectionPaths, { workspacePath }));
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -418,7 +418,7 @@ export const workspaceConfigUpdatedEvent = (workspacePath, workspaceUid, workspa
|
||||
|
||||
if (uniqueNewCollectionPaths.length > 0) {
|
||||
try {
|
||||
await dispatch(openMultipleCollections(uniqueNewCollectionPaths, { workspaceId: workspace.pathname }));
|
||||
await dispatch(openMultipleCollections(uniqueNewCollectionPaths, { workspacePath: workspace.pathname }));
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,14 @@ export const workspacesSlice = createSlice({
|
||||
if (workspace) {
|
||||
workspace.loadingState = loadingState;
|
||||
}
|
||||
},
|
||||
|
||||
workspaceDotEnvUpdateEvent: (state, action) => {
|
||||
const { workspaceUid, processEnvVariables } = action.payload;
|
||||
const workspace = state.workspaces.find((w) => w.uid === workspaceUid);
|
||||
if (workspace) {
|
||||
workspace.processEnvVariables = processEnvVariables;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -87,7 +95,8 @@ export const {
|
||||
updateWorkspace,
|
||||
addCollectionToWorkspace,
|
||||
removeCollectionFromWorkspace,
|
||||
updateWorkspaceLoadingState
|
||||
updateWorkspaceLoadingState,
|
||||
workspaceDotEnvUpdateEvent
|
||||
} = workspacesSlice.actions;
|
||||
|
||||
export default workspacesSlice.reducer;
|
||||
|
||||
@@ -99,6 +99,7 @@ export const exportCollection = (collection, version) => {
|
||||
|
||||
// delete process variables
|
||||
delete collection.processEnvVariables;
|
||||
delete collection.workspaceProcessEnvVariables;
|
||||
|
||||
deleteUidsInItems(collection.items);
|
||||
deleteUidsInEnvs(collection.environments);
|
||||
|
||||
@@ -1174,7 +1174,14 @@ export const getAllVariables = (collection, item) => {
|
||||
const pathParams = getPathParams(item);
|
||||
const { globalEnvironmentVariables = {} } = collection;
|
||||
|
||||
const { processEnvVariables = {}, runtimeVariables = {}, promptVariables = {} } = collection;
|
||||
const { processEnvVariables = {}, runtimeVariables = {}, promptVariables = {}, workspaceProcessEnvVariables = {} } = collection;
|
||||
|
||||
// Merge workspace and collection processEnvVariables (collection takes priority)
|
||||
const mergedProcessEnvVariables = {
|
||||
...workspaceProcessEnvVariables,
|
||||
...processEnvVariables
|
||||
};
|
||||
|
||||
const mergedVariables = {
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
@@ -1216,7 +1223,7 @@ export const getAllVariables = (collection, item) => {
|
||||
maskedEnvVariables: uniqueMaskedVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVariables
|
||||
...mergedProcessEnvVariables
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,9 +5,10 @@ const chokidar = require('chokidar');
|
||||
const yaml = require('js-yaml');
|
||||
const { generateUidBasedOnHash, uuid } = require('../utils/common');
|
||||
const { getWorkspaceUid } = require('../utils/workspace-config');
|
||||
const { parseEnvironment } = require('@usebruno/filestore');
|
||||
const { parseEnvironment, parseDotEnv } = require('@usebruno/filestore');
|
||||
const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||
const { decryptStringSafe } = require('../utils/encryption');
|
||||
const { setWorkspaceDotEnvVars, clearWorkspaceDotEnvVars } = require('../store/process-env');
|
||||
|
||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||
|
||||
@@ -122,15 +123,51 @@ const handleGlobalEnvironmentFileUnlink = async (win, pathname, workspaceUid) =>
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorkspaceDotEnvFile = (win, workspacePath, workspaceUid) => {
|
||||
try {
|
||||
const dotEnvPath = path.join(workspacePath, '.env');
|
||||
if (!fs.existsSync(dotEnvPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(dotEnvPath, 'utf8');
|
||||
const jsonData = parseDotEnv(content);
|
||||
|
||||
setWorkspaceDotEnvVars(workspacePath, jsonData);
|
||||
win.webContents.send('main:workspace-dotenv-update', {
|
||||
workspaceUid,
|
||||
workspacePath,
|
||||
processEnvVariables: { ...jsonData }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error handling workspace .env file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorkspaceDotEnvUnlink = (win, workspacePath, workspaceUid) => {
|
||||
try {
|
||||
clearWorkspaceDotEnvVars(workspacePath);
|
||||
win.webContents.send('main:workspace-dotenv-update', {
|
||||
workspaceUid,
|
||||
workspacePath,
|
||||
processEnvVariables: {}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error handling workspace .env file unlink:', error);
|
||||
}
|
||||
};
|
||||
|
||||
class WorkspaceWatcher {
|
||||
constructor() {
|
||||
this.watchers = {};
|
||||
this.environmentWatchers = {};
|
||||
this.dotEnvWatchers = {};
|
||||
}
|
||||
|
||||
addWatcher(win, workspacePath) {
|
||||
const workspaceFilePath = path.join(workspacePath, 'workspace.yml');
|
||||
const environmentsDir = path.join(workspacePath, 'environments');
|
||||
const dotEnvFilePath = path.join(workspacePath, '.env');
|
||||
const workspaceUid = getWorkspaceUid(workspacePath);
|
||||
|
||||
if (this.watchers[workspacePath]) {
|
||||
@@ -139,6 +176,9 @@ class WorkspaceWatcher {
|
||||
if (this.environmentWatchers[workspacePath]) {
|
||||
this.environmentWatchers[workspacePath].close();
|
||||
}
|
||||
if (this.dotEnvWatchers[workspacePath]) {
|
||||
this.dotEnvWatchers[workspacePath].close();
|
||||
}
|
||||
|
||||
const self = this;
|
||||
setTimeout(() => {
|
||||
@@ -146,6 +186,9 @@ class WorkspaceWatcher {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load initial .env file if exists
|
||||
handleWorkspaceDotEnvFile(win, workspacePath, workspaceUid);
|
||||
|
||||
const watcher = chokidar.watch(workspaceFilePath, {
|
||||
ignoreInitial: true,
|
||||
persistent: true,
|
||||
@@ -164,6 +207,22 @@ class WorkspaceWatcher {
|
||||
|
||||
self.watchers[workspacePath] = watcher;
|
||||
|
||||
const dotEnvWatcher = chokidar.watch(dotEnvFilePath, {
|
||||
ignoreInitial: true,
|
||||
persistent: true,
|
||||
ignorePermissionErrors: true,
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 80,
|
||||
pollInterval: 250
|
||||
}
|
||||
});
|
||||
|
||||
dotEnvWatcher.on('add', () => handleWorkspaceDotEnvFile(win, workspacePath, workspaceUid));
|
||||
dotEnvWatcher.on('change', () => handleWorkspaceDotEnvFile(win, workspacePath, workspaceUid));
|
||||
dotEnvWatcher.on('unlink', () => handleWorkspaceDotEnvUnlink(win, workspacePath, workspaceUid));
|
||||
|
||||
self.dotEnvWatchers[workspacePath] = dotEnvWatcher;
|
||||
|
||||
if (fs.existsSync(environmentsDir)) {
|
||||
const envWatcher = chokidar.watch(path.join(environmentsDir, `*.yml`), {
|
||||
ignoreInitial: true,
|
||||
@@ -216,6 +275,12 @@ class WorkspaceWatcher {
|
||||
this.environmentWatchers[workspacePath].close();
|
||||
delete this.environmentWatchers[workspacePath];
|
||||
}
|
||||
if (this.dotEnvWatchers[workspacePath]) {
|
||||
this.dotEnvWatchers[workspacePath].close();
|
||||
delete this.dotEnvWatchers[workspacePath];
|
||||
}
|
||||
// Clear workspace env vars when watcher is removed
|
||||
clearWorkspaceDotEnvVars(workspacePath);
|
||||
} catch (error) {
|
||||
console.error('Error removing workspace watcher:', error);
|
||||
}
|
||||
|
||||
@@ -823,9 +823,24 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:open-multiple-collections', async (e, collectionPaths) => {
|
||||
ipcMain.handle('renderer:open-multiple-collections', async (e, collectionPaths, options = {}) => {
|
||||
if (watcher && mainWindow) {
|
||||
await openCollectionsByPathname(mainWindow, watcher, collectionPaths);
|
||||
if (options.workspacePath) {
|
||||
const { setCollectionWorkspace } = require('../store/process-env');
|
||||
const { generateUidBasedOnHash } = require('../utils/common');
|
||||
for (const collectionPath of collectionPaths) {
|
||||
const collectionUid = generateUidBasedOnHash(collectionPath);
|
||||
setCollectionWorkspace(collectionUid, options.workspacePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:set-collection-workspace', (event, collectionUid, workspacePath) => {
|
||||
if (workspacePath) {
|
||||
const { setCollectionWorkspace } = require('../store/process-env');
|
||||
setCollectionWorkspace(collectionUid, workspacePath);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -838,6 +853,10 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
const { clearCollectionWorkspace } = require('../store/process-env');
|
||||
clearCollectionWorkspace(collectionUid);
|
||||
|
||||
if (workspacePath && workspacePath !== 'default') {
|
||||
try {
|
||||
const { removeCollectionFromWorkspace } = require('../utils/workspace-config');
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
/**
|
||||
* This file stores all the process.env variables under collection scope
|
||||
* This file stores all the process.env variables under collection and workspace scope
|
||||
*
|
||||
* process.env variables are sourced from 2 places:
|
||||
* 1. .env file in the root of the project
|
||||
* 2. process.env variables set in the OS
|
||||
* process.env variables are sourced from 3 places:
|
||||
* 1. .env file in the workspace root
|
||||
* 2. .env file in the collection root
|
||||
* 3. process.env variables set in the OS
|
||||
*
|
||||
* Priority (highest to lowest): collection .env > workspace .env > OS process.env
|
||||
*
|
||||
* Multiple collections can be opened in the same electron app.
|
||||
* Each collection's .env file can have different values for the same process.env variable.
|
||||
*/
|
||||
|
||||
const dotEnvVars = {};
|
||||
const workspaceDotEnvVars = {};
|
||||
const collectionWorkspaceMap = {};
|
||||
|
||||
// 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 workspacePath = collectionWorkspaceMap[collectionUid];
|
||||
const workspaceEnvVars = workspacePath ? workspaceDotEnvVars[workspacePath] : {};
|
||||
|
||||
// if there are .env vars for this collection, return the process.env merged with the .env vars
|
||||
return {
|
||||
...process.env,
|
||||
...workspaceEnvVars,
|
||||
...dotEnvVars[collectionUid]
|
||||
};
|
||||
};
|
||||
@@ -31,7 +32,27 @@ const setDotEnvVars = (collectionUid, envVars) => {
|
||||
dotEnvVars[collectionUid] = envVars;
|
||||
};
|
||||
|
||||
const setWorkspaceDotEnvVars = (workspacePath, envVars) => {
|
||||
workspaceDotEnvVars[workspacePath] = envVars;
|
||||
};
|
||||
|
||||
const clearWorkspaceDotEnvVars = (workspacePath) => {
|
||||
delete workspaceDotEnvVars[workspacePath];
|
||||
};
|
||||
|
||||
const setCollectionWorkspace = (collectionUid, workspacePath) => {
|
||||
collectionWorkspaceMap[collectionUid] = workspacePath;
|
||||
};
|
||||
|
||||
const clearCollectionWorkspace = (collectionUid) => {
|
||||
delete collectionWorkspaceMap[collectionUid];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getProcessEnvVars,
|
||||
setDotEnvVars
|
||||
setDotEnvVars,
|
||||
setWorkspaceDotEnvVars,
|
||||
clearWorkspaceDotEnvVars,
|
||||
setCollectionWorkspace,
|
||||
clearCollectionWorkspace
|
||||
};
|
||||
|
||||
@@ -610,6 +610,7 @@ const collectionSchema = Yup.object({
|
||||
items: Yup.array()
|
||||
}),
|
||||
runtimeVariables: Yup.object(),
|
||||
workspaceProcessEnvVariables: Yup.object().default({}),
|
||||
brunoConfig: Yup.object(),
|
||||
root: folderRootSchema
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user