From 5799513aa3845f98c62b9e5356a66dfae05116e6 Mon Sep 17 00:00:00 2001 From: Srikanth Patchava Date: Wed, 24 Jun 2026 23:38:49 -0700 Subject: [PATCH 1/2] perf: exclude node_modules from content globbing in monorepos In monorepo setups with hoisted/linked dependencies, Globby scans through node_modules directories when sourcing content, causing extreme startup slowness (34s to 330ms in the reported case). Changes: - Add **/node_modules/** to GlobExcludeDefault in globUtils.ts - Add ignore pattern to readCategoriesMetadata() in sidebars/index.ts which had no ignore patterns at all - Add test coverage for node_modules exclusion Fixes #12128 --- .../src/sidebars/index.ts | 1 + .../src/__tests__/globUtils.test.ts | 17 ++++++++++++++++- packages/docusaurus-utils/src/globUtils.ts | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts index 6af73d22ad0d..1828e9560124 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts @@ -43,6 +43,7 @@ export function resolveSidebarPathOption( async function readCategoriesMetadata(contentPath: string) { const categoryFiles = await Globby('**/_category_.{json,yml,yaml}', { cwd: contentPath, + ignore: ['**/node_modules/**'], }); const categoryToFile = _.groupBy(categoryFiles, path.dirname); return combinePromises( diff --git a/packages/docusaurus-utils/src/__tests__/globUtils.test.ts b/packages/docusaurus-utils/src/__tests__/globUtils.test.ts index 709671e916b1..e6f6c4c0bbb7 100644 --- a/packages/docusaurus-utils/src/__tests__/globUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/globUtils.test.ts @@ -27,6 +27,14 @@ describe('isTranslatableSourceFile', () => { }); describe('createMatcher', () => { + it('match default exclude node_modules correctly', () => { + const matcher = createMatcher(GlobExcludeDefault); + expect(matcher('node_modules/pkg/doc.md')).toBe(true); + expect(matcher('node_modules/pkg/index.js')).toBe(true); + expect(matcher('category/node_modules/pkg/doc.md')).toBe(true); + expect(matcher('category/node_modules/@scope/pkg/doc.md')).toBe(true); + }); + it('match default exclude MD/MDX partials correctly', () => { const matcher = createMatcher(GlobExcludeDefault); expect(matcher('doc.md')).toBe(false); @@ -115,8 +123,15 @@ describe('createAbsoluteFilePathMatcher', () => { expect(matcher('/root/_docs/_category/myDoc.mdx')).toBe(true); }); + it('match default exclude node_modules correctly', () => { + expect(matcher('/_root/docs/node_modules/pkg/doc.md')).toBe(true); + expect(matcher('/_root/docs/node_modules/@scope/pkg/doc.md')).toBe(true); + expect( + matcher('/_root/docs/category/node_modules/pkg/index.js'), + ).toBe(true); + }); + it('match default exclude tests correctly', () => { - expect(matcher('/__test__/website/src/xyz.js')).toBe(false); expect(matcher('/__test__/website/src/__test__/xyz.js')).toBe(true); expect(matcher('/__test__/website/src/xyz.test.js')).toBe(true); }); diff --git a/packages/docusaurus-utils/src/globUtils.ts b/packages/docusaurus-utils/src/globUtils.ts index c26e84ab65d7..99f109b4d2ed 100644 --- a/packages/docusaurus-utils/src/globUtils.ts +++ b/packages/docusaurus-utils/src/globUtils.ts @@ -26,6 +26,7 @@ export const Globby = Tinyglobby.glob; * - Ignore tests */ export const GlobExcludeDefault = [ + '**/node_modules/**', '**/_*.{js,jsx,ts,tsx,md,mdx}', '**/_*/**', '**/*.test.{js,jsx,ts,tsx}', From dc24d951d628ecb33a43bd4ec0c9d55bf203c7fe Mon Sep 17 00:00:00 2001 From: Srikanth Patchava Date: Fri, 26 Jun 2026 11:33:37 -0700 Subject: [PATCH 2/2] test: add edge-case test for content sourced from node_modules Add test proving that when contentPath is inside node_modules (e.g., node_modules/@myCompany/docs), content files at that root are NOT excluded. This works because relative paths computed from the root folder do not contain node_modules. Addresses review feedback from @slorber. Signed-off-by: Srikanth Patchava --- .../src/__tests__/globUtils.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/docusaurus-utils/src/__tests__/globUtils.test.ts b/packages/docusaurus-utils/src/__tests__/globUtils.test.ts index e6f6c4c0bbb7..644e3d54b7af 100644 --- a/packages/docusaurus-utils/src/__tests__/globUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/globUtils.test.ts @@ -33,6 +33,10 @@ describe('createMatcher', () => { expect(matcher('node_modules/pkg/index.js')).toBe(true); expect(matcher('category/node_modules/pkg/doc.md')).toBe(true); expect(matcher('category/node_modules/@scope/pkg/doc.md')).toBe(true); + // When contentPath is inside node_modules, relative paths of content + // files do NOT contain node_modules, so they should NOT be excluded. + expect(matcher('intro.md')).toBe(false); + expect(matcher('guide/setup.mdx')).toBe(false); }); it('match default exclude MD/MDX partials correctly', () => { @@ -131,7 +135,30 @@ describe('createAbsoluteFilePathMatcher', () => { ).toBe(true); }); + // Ensures that when contentPath is inside node_modules (e.g., + // node_modules/@myCompany/docs), content files at that root are NOT + // excluded, because their relative paths don't contain node_modules. + it('does not exclude content when root folder is inside node_modules', () => { + const nmMatcher = createAbsoluteFilePathMatcher(GlobExcludeDefault, [ + '/project/node_modules/@myCompany/docs', + ]); + // Content at the root should NOT be excluded + expect( + nmMatcher('/project/node_modules/@myCompany/docs/intro.md'), + ).toBe(false); + expect( + nmMatcher('/project/node_modules/@myCompany/docs/guide/setup.mdx'), + ).toBe(false); + // But nested node_modules inside that root SHOULD be excluded + expect( + nmMatcher( + '/project/node_modules/@myCompany/docs/node_modules/dep/file.md', + ), + ).toBe(true); + }); + it('match default exclude tests correctly', () => { + expect(matcher('/__test__/website/src/xyz.js')).toBe(false); expect(matcher('/__test__/website/src/__test__/xyz.js')).toBe(true); expect(matcher('/__test__/website/src/xyz.test.js')).toBe(true); });