From 2f5537c8dbcb0c8d0b7ebdd97914f27842b71428 Mon Sep 17 00:00:00 2001 From: sanish chirayath Date: Fri, 12 Dec 2025 19:04:52 +0530 Subject: [PATCH] Enhance file watching by ensuring 'node_modules' and '.git' are always ignored (#6391) * Enhance file watching by ensuring 'node_modules' and '.git' are always ignored * fix: tests * rm: dialog assignments * fix: duplication --- .../bruno-electron/src/app/apiSpecsWatcher.js | 13 +- .../src/app/collection-watcher.js | 13 +- .../bruno-electron/src/app/collections.js | 12 +- .../default-ignores/default-ignores.spec.ts | 185 ++++++++++++++++++ 4 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 tests/collection/default-ignores/default-ignores.spec.ts diff --git a/packages/bruno-electron/src/app/apiSpecsWatcher.js b/packages/bruno-electron/src/app/apiSpecsWatcher.js index 73f210cd0..b8ce55a62 100644 --- a/packages/bruno-electron/src/app/apiSpecsWatcher.js +++ b/packages/bruno-electron/src/app/apiSpecsWatcher.js @@ -85,7 +85,12 @@ class ApiSpecWatcher { this.watcherWorkspaces[watchPath] = workspacePath; } - const ignores = brunoConfig?.ignore || []; + // Always ignore node_modules and .git, regardless of user config + // This prevents infinite loops with symlinked directories (e.g., npm workspaces) + const defaultIgnores = ['node_modules', '.git']; + const userIgnores = brunoConfig?.ignore || []; + const ignores = [...new Set([...defaultIgnores, ...userIgnores])]; + const self = this; setTimeout(() => { const watcher = chokidar.watch(watchPath, { @@ -95,6 +100,12 @@ class ApiSpecWatcher { const normalizedPath = filepath.replace(/\\/g, '/'); const relativePath = path.relative(watchPath, normalizedPath); + // Check if any path segment matches a default ignore pattern (handles symlinks) + const pathSegments = relativePath.split(path.sep); + if (pathSegments.some((segment) => defaultIgnores.includes(segment))) { + return true; + } + return ignores.some((ignorePattern) => { const normalizedIgnorePattern = ignorePattern.replace(/\\/g, '/'); return relativePath === normalizedIgnorePattern || relativePath.startsWith(normalizedIgnorePattern); diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js index c8a88dd77..036fc7414 100644 --- a/packages/bruno-electron/src/app/collection-watcher.js +++ b/packages/bruno-electron/src/app/collection-watcher.js @@ -745,7 +745,12 @@ class CollectionWatcher { this.startCollectionDiscovery(win, collectionUid); - const ignores = brunoConfig?.ignore || []; + // Always ignore node_modules and .git, regardless of user config + // This prevents infinite loops with symlinked directories (e.g., npm workspaces) + const defaultIgnores = ['node_modules', '.git']; + const userIgnores = brunoConfig?.ignore || []; + const ignores = [...new Set([...defaultIgnores, ...userIgnores])]; + setTimeout(() => { const watcher = chokidar.watch(watchPath, { ignoreInitial: false, @@ -754,6 +759,12 @@ class CollectionWatcher { const normalizedPath = normalizeAndResolvePath(filepath); const relativePath = path.relative(watchPath, normalizedPath); + // Check if any path segment matches a default ignore pattern (handles symlinks) + const pathSegments = relativePath.split(path.sep); + if (pathSegments.some((segment) => defaultIgnores.includes(segment))) { + return true; + } + return ignores.some((ignorePattern) => { return relativePath === ignorePattern || relativePath.startsWith(ignorePattern); }); diff --git a/packages/bruno-electron/src/app/collections.js b/packages/bruno-electron/src/app/collections.js index f86d2cece..ed9388b69 100644 --- a/packages/bruno-electron/src/app/collections.js +++ b/packages/bruno-electron/src/app/collections.js @@ -103,13 +103,11 @@ const openCollection = async (win, watcher, collectionPath, options = {}) => { let brunoConfig = await getCollectionConfigFile(collectionPath); const uid = generateUidBasedOnHash(collectionPath); - if (!brunoConfig.ignore || brunoConfig.ignore.length === 0) { - // 5 Feb 2024: - // bruno.json now supports an "ignore" field to specify which folders to ignore - // if the ignore field is not present, we default to ignoring node_modules and .git - // this is to maintain backwards compatibility with older collections - brunoConfig.ignore = ['node_modules', '.git']; - } + // Always ensure node_modules and .git are ignored, regardless of user config + // This prevents infinite loops with symlinked directories (e.g., npm workspaces) + const defaultIgnores = ['node_modules', '.git']; + const userIgnores = brunoConfig.ignore || []; + brunoConfig.ignore = [...new Set([...defaultIgnores, ...userIgnores])]; // Transform the config to add existence checks for protobuf files and import paths brunoConfig = await transformBrunoConfigAfterRead(brunoConfig, collectionPath); diff --git a/tests/collection/default-ignores/default-ignores.spec.ts b/tests/collection/default-ignores/default-ignores.spec.ts new file mode 100644 index 000000000..3c5a8fdb7 --- /dev/null +++ b/tests/collection/default-ignores/default-ignores.spec.ts @@ -0,0 +1,185 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; +import * as fs from 'fs'; +import { closeAllCollections, openCollectionAndAcceptSandbox } from '../../utils/page'; +import { buildCommonLocators } from '../../utils/page/locators'; + +test.describe('Default ignores for node_modules and .git', () => { + test.afterEach(async ({ page }) => { + await closeAllCollections(page); + }); + + test('Should always ignore node_modules even when user has custom ignore config', async ({ + page, + electronApp, + createTmpDir + }) => { + const locators = buildCommonLocators(page); + const collectionDir = await createTmpDir('node-modules-ignore-test'); + + // Create bruno.json with custom ignore that doesn't include node_modules + const brunoConfig = { + version: '1', + name: 'Node Modules Ignore Test', + type: 'collection', + ignore: ['custom-folder', 'another-folder'] // Explicitly NOT including node_modules + }; + fs.writeFileSync(path.join(collectionDir, 'bruno.json'), JSON.stringify(brunoConfig, null, 2)); + + // Create node_modules directory with .bru files inside + const nodeModulesDir = path.join(collectionDir, 'node_modules'); + fs.mkdirSync(nodeModulesDir); + fs.mkdirSync(path.join(nodeModulesDir, 'some-package')); + + // Create a .bru file inside node_modules (should be ignored) + fs.writeFileSync( + path.join(nodeModulesDir, 'some-package', 'fake-request.bru'), + `meta { + name: Fake Request In Node Modules + type: http + seq: 1 +} + +get { + url: https://fake.com + body: none + auth: none +} +` + ); + + // Create a real request at the collection root + fs.writeFileSync( + path.join(collectionDir, 'real-request.bru'), + `meta { + name: Real Request + type: http + seq: 1 +} + +get { + url: https://real.com + body: none + auth: none +} +` + ); + + // Mock the electron dialog + await electronApp.evaluate( + ({ dialog }, { collectionDir }) => { + dialog.showOpenDialog = async () => ({ + canceled: false, + filePaths: [collectionDir] + }); + }, + { collectionDir } + ); + + // Open the collection + await locators.plusMenu.button().click(); + await locators.dropdown.tippyItem('Open collection').click(); + + // Wait for collection to load + await expect(locators.sidebar.collection('Node Modules Ignore Test')).toBeVisible({ timeout: 30000 }); + + // Accept the sandbox mode + await openCollectionAndAcceptSandbox(page, 'Node Modules Ignore Test', 'safe'); + + // Verify only the real request is visible + await expect(locators.sidebar.request('Real Request')).toBeVisible({ timeout: 10000 }); + + // The fake request inside node_modules should NOT be visible + await expect(locators.sidebar.request('Fake Request In Node Modules')).not.toBeVisible(); + + // node_modules folder should not appear in the sidebar + await expect(locators.sidebar.folder('node_modules')).not.toBeVisible(); + }); + + test('Should always ignore .git even when user has custom ignore config', async ({ + page, + electronApp, + createTmpDir + }) => { + const locators = buildCommonLocators(page); + const collectionDir = await createTmpDir('git-ignore-test'); + + // Create bruno.json with custom ignore that doesn't include .git + const brunoConfig = { + version: '1', + name: 'Git Ignore Test', + type: 'collection', + ignore: ['custom-folder'] // Explicitly NOT including .git + }; + fs.writeFileSync(path.join(collectionDir, 'bruno.json'), JSON.stringify(brunoConfig, null, 2)); + + // Create .git directory with .bru files inside + const gitDir = path.join(collectionDir, '.git'); + fs.mkdirSync(gitDir); + fs.mkdirSync(path.join(gitDir, 'hooks')); + + // Create a .bru file inside .git (should be ignored) + fs.writeFileSync( + path.join(gitDir, 'hooks', 'fake-git-request.bru'), + `meta { + name: Fake Request In Git + type: http + seq: 1 +} + +get { + url: https://fake-git.com + body: none + auth: none +} +` + ); + + // Create a real request at the collection root + fs.writeFileSync( + path.join(collectionDir, 'real-request.bru'), + `meta { + name: Real Git Request + type: http + seq: 1 +} + +get { + url: https://real.com + body: none + auth: none +} +` + ); + + // Mock the electron dialog + await electronApp.evaluate( + ({ dialog }, { collectionDir }) => { + dialog.showOpenDialog = async () => ({ + canceled: false, + filePaths: [collectionDir] + }); + }, + { collectionDir } + ); + + // Open the collection + await locators.plusMenu.button().click(); + await locators.dropdown.tippyItem('Open collection').click(); + + // Wait for collection to load + await expect(locators.sidebar.collection('Git Ignore Test')).toBeVisible({ timeout: 30000 }); + + // Accept the sandbox mode + await openCollectionAndAcceptSandbox(page, 'Git Ignore Test', 'safe'); + + // Verify only the real request is visible + await expect(locators.sidebar.request('Real Git Request')).toBeVisible({ timeout: 10000 }); + + // The fake request inside .git should NOT be visible + await expect(locators.sidebar.request('Fake Request In Git')).not.toBeVisible(); + + // .git folder should not appear in the sidebar + await expect(locators.sidebar.folder('.git')).not.toBeVisible(); + }); +});