Files
bruno/packages/bruno-electron/src/app/dotenv-watcher.js
2026-05-14 17:38:55 +05:30

219 lines
5.7 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const chokidar = require('chokidar');
const { parseDotEnv } = require('@usebruno/filestore');
const { setDotEnvVars, clearDotEnvVars, setWorkspaceDotEnvVars, clearWorkspaceDotEnvVars } = require('../store/process-env');
const isDotEnvFile = (filename) => {
return filename === '.env' || filename.startsWith('.env.');
};
const parseVariablesToArray = (envObject) => {
return Object.entries(envObject).map(([name, value]) => ({
name,
value,
enabled: true,
secret: false
}));
};
const DEFAULT_WATCHER_OPTIONS = {
ignoreInitial: false,
persistent: true,
ignorePermissionErrors: true,
depth: 0
};
const createFileHandler = (win, options) => (pathname) => {
const { type, uid, uidKey, pathKey, basePath, setEnvVars } = options;
const filename = path.basename(pathname);
if (!isDotEnvFile(filename)) {
return;
}
try {
const content = fs.readFileSync(pathname, 'utf8');
const jsonData = parseDotEnv(content);
if (filename === '.env') {
setEnvVars(jsonData);
}
const variables = parseVariablesToArray(jsonData);
if (!win.isDestroyed()) {
const payload = {
type,
[uidKey]: uid,
filename,
variables,
exists: true,
processEnvVariables: { ...jsonData }
};
if (pathKey) {
payload[pathKey] = basePath;
}
win.webContents.send('main:dotenv-file-update', payload);
}
} catch (err) {
console.error(`Error processing dotenv file ${pathname}:`, err);
}
};
const createUnlinkHandler = (win, options) => (pathname) => {
const { type, uid, uidKey, pathKey, basePath, clearEnvVars } = options;
const filename = path.basename(pathname);
if (!isDotEnvFile(filename)) {
return;
}
if (filename === '.env') {
clearEnvVars();
}
if (!win.isDestroyed()) {
const payload = {
type,
[uidKey]: uid,
filename,
variables: [],
exists: false,
processEnvVariables: {}
};
if (pathKey) {
payload[pathKey] = basePath;
}
win.webContents.send('main:dotenv-file-update', payload);
}
};
class DotEnvWatcher {
constructor() {
this.collectionWatchers = new Map();
this.workspaceWatchers = new Map();
}
addCollectionWatcher(win, collectionPath, collectionUid) {
if (this.collectionWatchers.has(collectionPath)) {
this.collectionWatchers.get(collectionPath).close();
}
const watcher = chokidar.watch(collectionPath, {
...DEFAULT_WATCHER_OPTIONS,
disableGlobbing: true,
awaitWriteFinish: {
stabilityThreshold: 80,
pollInterval: 100
}
});
const handlerOptions = {
type: 'collection',
uid: collectionUid,
uidKey: 'collectionUid',
basePath: collectionPath,
setEnvVars: (data) => setDotEnvVars(collectionUid, data),
clearEnvVars: () => clearDotEnvVars(collectionUid)
};
const handleFile = createFileHandler(win, handlerOptions);
const handleUnlink = createUnlinkHandler(win, handlerOptions);
watcher.on('add', handleFile);
watcher.on('change', handleFile);
watcher.on('unlink', handleUnlink);
watcher.on('error', (err) => {
console.error(`Collection watcher error for ${collectionPath}:`, err);
});
this.collectionWatchers.set(collectionPath, watcher);
}
removeCollectionWatcher(collectionPath, collectionUid) {
if (this.collectionWatchers.has(collectionPath)) {
this.collectionWatchers.get(collectionPath).close();
this.collectionWatchers.delete(collectionPath);
}
if (collectionUid) {
clearDotEnvVars(collectionUid);
}
}
hasCollectionWatcher(collectionPath) {
return this.collectionWatchers.has(collectionPath);
}
addWorkspaceWatcher(win, workspacePath, workspaceUid) {
if (this.workspaceWatchers.has(workspacePath)) {
this.workspaceWatchers.get(workspacePath).close();
}
const watcher = chokidar.watch(workspacePath, {
...DEFAULT_WATCHER_OPTIONS,
disableGlobbing: true,
awaitWriteFinish: {
stabilityThreshold: 80,
pollInterval: 250
}
});
const handlerOptions = {
type: 'workspace',
uid: workspaceUid,
uidKey: 'workspaceUid',
pathKey: 'workspacePath',
basePath: workspacePath,
setEnvVars: (data) => setWorkspaceDotEnvVars(workspacePath, data),
clearEnvVars: () => clearWorkspaceDotEnvVars(workspacePath)
};
const handleFile = createFileHandler(win, handlerOptions);
const handleUnlink = createUnlinkHandler(win, handlerOptions);
watcher.on('add', handleFile);
watcher.on('change', handleFile);
watcher.on('unlink', handleUnlink);
watcher.on('error', (err) => {
console.error(`Workspace watcher error for ${workspacePath}:`, err);
});
this.workspaceWatchers.set(workspacePath, watcher);
}
removeWorkspaceWatcher(workspacePath) {
if (this.workspaceWatchers.has(workspacePath)) {
this.workspaceWatchers.get(workspacePath).close();
this.workspaceWatchers.delete(workspacePath);
}
clearWorkspaceDotEnvVars(workspacePath);
}
hasWorkspaceWatcher(workspacePath) {
return this.workspaceWatchers.has(workspacePath);
}
closeAll() {
const pending = [];
const collect = (watcher) => {
try {
const result = watcher?.close();
if (result && typeof result.then === 'function') pending.push(result);
} catch (err) {}
};
for (const [path, watcher] of this.collectionWatchers) collect(watcher);
this.collectionWatchers.clear();
for (const [path, watcher] of this.workspaceWatchers) collect(watcher);
this.workspaceWatchers.clear();
return Promise.allSettled(pending);
}
}
const dotEnvWatcher = new DotEnvWatcher();
module.exports = dotEnvWatcher;