From 73355cb86d7903054af268d1cc193f83b7995212 Mon Sep 17 00:00:00 2001 From: sebastien Date: Mon, 2 Nov 2026 10:45:42 +0100 Subject: [PATCH 1/2] refactor: replace lodash helpers that have a native equivalent (AI-assisted) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `_.last()` → `Array.prototype.at(-1)` - `_.uniq()` → `new Set()` - `_.groupBy()` → `Map.groupBy()` where we iterate the groups - `_.difference()` / `_.intersection()` → `Array.prototype.filter()` Co-Authored-By: Claude Opus 5.5 --- .../src/collectRedirects.ts | 4 +-- .../src/sidebars/generator.ts | 2 +- .../src/sidebars/utils.ts | 7 ++-- .../src/translations.ts | 6 ++-- .../src/versions/validation.ts | 11 +++--- packages/docusaurus-utils/src/tags.ts | 2 +- .../src/commands/swizzle/actions.ts | 2 +- .../src/commands/swizzle/components.ts | 6 ++-- .../docusaurus/src/commands/swizzle/themes.ts | 16 +++++---- .../src/server/codegen/codegenRoutes.ts | 6 ++-- .../src/server/plugins/pluginIds.ts | 35 +++++++++---------- .../src/server/translations/translations.ts | 10 +++--- 12 files changed, 52 insertions(+), 55 deletions(-) diff --git a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts index 225ab35c118f..65d76173de99 100644 --- a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts +++ b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts @@ -169,8 +169,8 @@ function filterUnwantedRedirects( ): RedirectItem[] { // We don't want to create the same redirect twice, since that would lead to // writing the same html redirection file twice. - Object.entries(_.groupBy(redirects, (redirect) => redirect.from)).forEach( - ([from, groupedFromRedirects]) => { + Map.groupBy(redirects, (redirect) => redirect.from).forEach( + (groupedFromRedirects, from) => { if (groupedFromRedirects.length > 1) { logger.report( pluginContext.siteConfig.onDuplicateRoutes, diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index 3ab888317df4..d478cdc5999e 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -23,7 +23,7 @@ const BreadcrumbSeparator = '/'; // Just an alias to the make code more explicit function getLocalDocId(docId: string): string { - return _.last(docId.split('/'))!; + return docId.split('/').at(-1)!; } export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}'; diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts index 3ed0e06099f8..d79be363ff1a 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts @@ -308,7 +308,10 @@ export function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils { sidebarFilePath: string; }) { const allSidebarDocIds = Object.values(sidebarNameToDocIds).flat(); - const invalidDocIds = _.difference(allSidebarDocIds, allDocIds); + const existingDocIds = new Set(allDocIds); + const invalidDocIds = allSidebarDocIds.filter( + (docId) => !existingDocIds.has(docId), + ); if (invalidDocIds.length > 0) { throw new Error( @@ -319,7 +322,7 @@ These sidebar document ids do not exist: - ${invalidDocIds.sort().join('\n- ')} Available document ids are: -- ${_.uniq(allDocIds).sort().join('\n- ')} +- ${[...existingDocIds].sort().join('\n- ')} `, ); } diff --git a/packages/docusaurus-plugin-content-docs/src/translations.ts b/packages/docusaurus-plugin-content-docs/src/translations.ts index 4fa0801c52b9..5250f719fe8f 100644 --- a/packages/docusaurus-plugin-content-docs/src/translations.ts +++ b/packages/docusaurus-plugin-content-docs/src/translations.ts @@ -46,10 +46,8 @@ type TranslationMessageEntry = [string, TranslationMessage]; function ensureNoSidebarDuplicateEntries( translationEntries: TranslationMessageEntry[], ): void { - const grouped = _.groupBy(translationEntries, (entry) => entry[0]); - const duplicates = Object.entries(grouped).filter( - (entry) => entry[1].length > 1, - ); + const grouped = Map.groupBy(translationEntries, (entry) => entry[0]); + const duplicates = [...grouped].filter((entry) => entry[1].length > 1); if (duplicates.length > 0) { throw new Error(`Multiple docs sidebar items produce the same translation key. diff --git a/packages/docusaurus-plugin-content-docs/src/versions/validation.ts b/packages/docusaurus-plugin-content-docs/src/versions/validation.ts index c2f1505275a0..e668d13e8d8a 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions/validation.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions/validation.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import _ from 'lodash'; import type {VersionsOptions} from '@docusaurus/plugin-content-docs'; export function validateVersionName(name: unknown): asserts name is string { @@ -74,9 +73,8 @@ export function validateVersionsOptions( `Docs option lastVersion: ${options.lastVersion} is invalid. ${availableVersionNamesMsg}`, ); } - const unknownVersionConfigNames = _.difference( - Object.keys(options.versions), - availableVersionNames, + const unknownVersionConfigNames = Object.keys(options.versions).filter( + (versionName) => !availableVersionNames.includes(versionName), ); if (unknownVersionConfigNames.length > 0) { throw new Error( @@ -92,9 +90,8 @@ export function validateVersionsOptions( `Invalid docs option "onlyIncludeVersions": an empty array is not allowed, at least one version is needed.`, ); } - const unknownOnlyIncludeVersionNames = _.difference( - options.onlyIncludeVersions, - availableVersionNames, + const unknownOnlyIncludeVersionNames = options.onlyIncludeVersions.filter( + (versionName) => !availableVersionNames.includes(versionName), ); if (unknownOnlyIncludeVersionNames.length > 0) { throw new Error( diff --git a/packages/docusaurus-utils/src/tags.ts b/packages/docusaurus-utils/src/tags.ts index c221f8a26b87..24a88e025cc7 100644 --- a/packages/docusaurus-utils/src/tags.ts +++ b/packages/docusaurus-utils/src/tags.ts @@ -191,7 +191,7 @@ export function groupTaggedItems( // If user add twice the same tag to a md doc (weird but possible), // we don't want the item to appear twice in the list... Object.values(result).forEach((group) => { - group.items = _.uniq(group.items); + group.items = [...new Set(group.items)]; }); return result; diff --git a/packages/docusaurus/src/commands/swizzle/actions.ts b/packages/docusaurus/src/commands/swizzle/actions.ts index 28cce032afc6..8f420ca11156 100644 --- a/packages/docusaurus/src/commands/swizzle/actions.ts +++ b/packages/docusaurus/src/commands/swizzle/actions.ts @@ -121,7 +121,7 @@ export async function wrap({ const isDirectory = await isDir(path.join(themePath, themeComponentName)); // Top/Parent/ComponentName => ComponentName - const componentName = _.last(themeComponentName.split('/'))!; + const componentName = themeComponentName.split('/').at(-1)!; const wrapperComponentName = `${componentName}Wrapper`; const wrapperFileName = `${themeComponentName}${isDirectory ? '/index' : ''}${ diff --git a/packages/docusaurus/src/commands/swizzle/components.ts b/packages/docusaurus/src/commands/swizzle/components.ts index 30ed943f1035..4fffafeb4686 100644 --- a/packages/docusaurus/src/commands/swizzle/components.ts +++ b/packages/docusaurus/src/commands/swizzle/components.ts @@ -58,13 +58,15 @@ export function getMissingIntermediateComponentFolderNames( return _.range(1, paths.length + 1).map((i) => paths.slice(0, i).join('/')); } - const expandedComponentNames = _.uniq( + const expandedComponentNames = new Set( componentNames.flatMap((componentName) => getAllIntermediatePaths(componentName), ), ); - return _.difference(expandedComponentNames, componentNames); + return [...expandedComponentNames].filter( + (componentName) => !componentNames.includes(componentName), + ); } const skipReadDirNames = ['__test__', '__tests__', '__mocks__', '__fixtures__']; diff --git a/packages/docusaurus/src/commands/swizzle/themes.ts b/packages/docusaurus/src/commands/swizzle/themes.ts index 369f2211dcb7..c8cef045599e 100644 --- a/packages/docusaurus/src/commands/swizzle/themes.ts +++ b/packages/docusaurus/src/commands/swizzle/themes.ts @@ -33,13 +33,15 @@ export function getPluginByThemeName( } export function getThemeNames(plugins: SwizzlePlugin[]): string[] { - const themeNames = _.uniq( - // The fact that getThemePath is attached to the plugin instance makes - // this code impossible to optimize. If this is a static method, we don't - // need to initialize all plugins just to filter which are themes - // Benchmark: loadContext-58ms; initPlugins-323ms - plugins.map((plugin) => pluginToThemeName(plugin)).filter(Boolean), - ) as string[]; + const themeNames = [ + ...new Set( + // The fact that getThemePath is attached to the plugin instance makes + // this code impossible to optimize. If this is a static method, we don't + // need to initialize all plugins just to filter which are themes + // Benchmark: loadContext-58ms; initPlugins-323ms + plugins.map((plugin) => pluginToThemeName(plugin)).filter(Boolean), + ), + ] as string[]; // Opinionated ordering: user is most likely to swizzle: // - the classic theme diff --git a/packages/docusaurus/src/server/codegen/codegenRoutes.ts b/packages/docusaurus/src/server/codegen/codegenRoutes.ts index 9f6a9dc1c0d2..8ab3ba9374ca 100644 --- a/packages/docusaurus/src/server/codegen/codegenRoutes.ts +++ b/packages/docusaurus/src/server/codegen/codegenRoutes.ts @@ -405,9 +405,9 @@ function ensureNoPropsConflict(route: RouteConfig) { if (!route.props && !route.modules) { return; } - const conflictingPropNames = _.intersection( - Object.keys(route.props ?? {}), - Object.keys(route.modules ?? {}), + const moduleNames = Object.keys(route.modules ?? {}); + const conflictingPropNames = Object.keys(route.props ?? {}).filter( + (propName) => moduleNames.includes(propName), ); if (conflictingPropNames.length > 0) { throw new Error( diff --git a/packages/docusaurus/src/server/plugins/pluginIds.ts b/packages/docusaurus/src/server/plugins/pluginIds.ts index 86e3eb5d9f97..82f97b38fc29 100644 --- a/packages/docusaurus/src/server/plugins/pluginIds.ts +++ b/packages/docusaurus/src/server/plugins/pluginIds.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import _ from 'lodash'; import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils'; import type {InitializedPlugin} from '@docusaurus/types'; @@ -16,26 +15,24 @@ import type {InitializedPlugin} from '@docusaurus/types'; export function ensureUniquePluginInstanceIds( plugins: InitializedPlugin[], ): void { - const pluginsByName = _.groupBy(plugins, (p) => p.name); - Object.entries(pluginsByName).forEach(([pluginName, pluginInstances]) => { - const pluginInstancesById = _.groupBy( + const pluginsByName = Map.groupBy(plugins, (p) => p.name); + pluginsByName.forEach((pluginInstances, pluginName) => { + const pluginInstancesById = Map.groupBy( pluginInstances, (p) => p.options.id ?? DEFAULT_PLUGIN_ID, ); - Object.entries(pluginInstancesById).forEach( - ([pluginId, pluginInstancesWithId]) => { - if (pluginInstancesWithId.length !== 1) { - throw new Error( - `Plugin "${pluginName}" is used ${ - pluginInstancesWithId.length - } times with ID "${pluginId}".\nTo use the same plugin multiple times on a Docusaurus site, you need to assign a unique ID to each plugin instance.${ - pluginId === DEFAULT_PLUGIN_ID - ? `\n\nThe plugin ID is "${DEFAULT_PLUGIN_ID}" by default. It's possible that the preset you are using already includes a plugin instance, in which case you either want to disable the plugin in the preset (to use a single instance), or assign another ID to your extra plugin instance (to use multiple instances).` - : '' - }`, - ); - } - }, - ); + pluginInstancesById.forEach((pluginInstancesWithId, pluginId) => { + if (pluginInstancesWithId.length !== 1) { + throw new Error( + `Plugin "${pluginName}" is used ${ + pluginInstancesWithId.length + } times with ID "${pluginId}".\nTo use the same plugin multiple times on a Docusaurus site, you need to assign a unique ID to each plugin instance.${ + pluginId === DEFAULT_PLUGIN_ID + ? `\n\nThe plugin ID is "${DEFAULT_PLUGIN_ID}" by default. It's possible that the preset you are using already includes a plugin instance, in which case you either want to disable the plugin in the preset (to use a single instance), or assign another ID to your extra plugin instance (to use multiple instances).` + : '' + }`, + ); + } + }); }); } diff --git a/packages/docusaurus/src/server/translations/translations.ts b/packages/docusaurus/src/server/translations/translations.ts index cfee61b004b0..91b350b701ef 100644 --- a/packages/docusaurus/src/server/translations/translations.ts +++ b/packages/docusaurus/src/server/translations/translations.ts @@ -113,9 +113,8 @@ async function writeTranslationFileContent({ const existingContent = await readTranslationFileContent(filePath); // Warn about potential legacy keys - const unknownKeys = _.difference( - Object.keys(existingContent ?? {}), - Object.keys(newContent), + const unknownKeys = Object.keys(existingContent ?? {}).filter( + (key) => !Object.hasOwn(newContent, key), ); if (unknownKeys.length > 0) { logger.warn`Some translation keys looks unknown to us in file path=${filePath}. @@ -276,9 +275,8 @@ export function applyDefaultCodeTranslations({ extractedCodeTranslations: TranslationFileContent; defaultCodeMessages: CodeTranslations; }): TranslationFileContent { - const unusedDefaultCodeMessages = _.difference( - Object.keys(defaultCodeMessages), - Object.keys(extractedCodeTranslations), + const unusedDefaultCodeMessages = Object.keys(defaultCodeMessages).filter( + (messageId) => !Object.hasOwn(extractedCodeTranslations, messageId), ); if (unusedDefaultCodeMessages.length > 0) { logger.warn`Unused default message codes found. From 9f1d308eb4249fc7da705360f74ca40d58b4cf88 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 5 Nov 2026 11:29:51 +0100 Subject: [PATCH 2/2] refactor: use Set difference() and intersection() instead of filter() (AI-assisted) Co-Authored-By: Claude Opus 5.5 --- .../src/sidebars/utils.ts | 6 ++---- .../src/versions/validation.ts | 13 +++++++------ .../docusaurus/src/commands/swizzle/components.ts | 4 +--- .../docusaurus/src/server/codegen/codegenRoutes.ts | 7 +++---- .../src/server/translations/translations.ts | 14 ++++++++------ 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts index d79be363ff1a..211e8f2690cc 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts @@ -307,11 +307,9 @@ export function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils { allDocIds: string[]; sidebarFilePath: string; }) { - const allSidebarDocIds = Object.values(sidebarNameToDocIds).flat(); + const allSidebarDocIds = new Set(Object.values(sidebarNameToDocIds).flat()); const existingDocIds = new Set(allDocIds); - const invalidDocIds = allSidebarDocIds.filter( - (docId) => !existingDocIds.has(docId), - ); + const invalidDocIds = [...allSidebarDocIds.difference(existingDocIds)]; if (invalidDocIds.length > 0) { throw new Error( diff --git a/packages/docusaurus-plugin-content-docs/src/versions/validation.ts b/packages/docusaurus-plugin-content-docs/src/versions/validation.ts index e668d13e8d8a..314d594845b0 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions/validation.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions/validation.ts @@ -73,9 +73,10 @@ export function validateVersionsOptions( `Docs option lastVersion: ${options.lastVersion} is invalid. ${availableVersionNamesMsg}`, ); } - const unknownVersionConfigNames = Object.keys(options.versions).filter( - (versionName) => !availableVersionNames.includes(versionName), - ); + const availableVersions = new Set(availableVersionNames); + const unknownVersionConfigNames = [ + ...new Set(Object.keys(options.versions)).difference(availableVersions), + ]; if (unknownVersionConfigNames.length > 0) { throw new Error( `Invalid docs option "versions": unknown versions (${unknownVersionConfigNames.join( @@ -90,9 +91,9 @@ export function validateVersionsOptions( `Invalid docs option "onlyIncludeVersions": an empty array is not allowed, at least one version is needed.`, ); } - const unknownOnlyIncludeVersionNames = options.onlyIncludeVersions.filter( - (versionName) => !availableVersionNames.includes(versionName), - ); + const unknownOnlyIncludeVersionNames = [ + ...new Set(options.onlyIncludeVersions).difference(availableVersions), + ]; if (unknownOnlyIncludeVersionNames.length > 0) { throw new Error( `Invalid docs option "onlyIncludeVersions": unknown versions (${unknownOnlyIncludeVersionNames.join( diff --git a/packages/docusaurus/src/commands/swizzle/components.ts b/packages/docusaurus/src/commands/swizzle/components.ts index 4fffafeb4686..4927a6b09dc8 100644 --- a/packages/docusaurus/src/commands/swizzle/components.ts +++ b/packages/docusaurus/src/commands/swizzle/components.ts @@ -64,9 +64,7 @@ export function getMissingIntermediateComponentFolderNames( ), ); - return [...expandedComponentNames].filter( - (componentName) => !componentNames.includes(componentName), - ); + return [...expandedComponentNames.difference(new Set(componentNames))]; } const skipReadDirNames = ['__test__', '__tests__', '__mocks__', '__fixtures__']; diff --git a/packages/docusaurus/src/server/codegen/codegenRoutes.ts b/packages/docusaurus/src/server/codegen/codegenRoutes.ts index 8ab3ba9374ca..2603fc5b9941 100644 --- a/packages/docusaurus/src/server/codegen/codegenRoutes.ts +++ b/packages/docusaurus/src/server/codegen/codegenRoutes.ts @@ -405,10 +405,9 @@ function ensureNoPropsConflict(route: RouteConfig) { if (!route.props && !route.modules) { return; } - const moduleNames = Object.keys(route.modules ?? {}); - const conflictingPropNames = Object.keys(route.props ?? {}).filter( - (propName) => moduleNames.includes(propName), - ); + const propNames = new Set(Object.keys(route.props ?? {})); + const moduleNames = new Set(Object.keys(route.modules ?? {})); + const conflictingPropNames = [...propNames.intersection(moduleNames)]; if (conflictingPropNames.length > 0) { throw new Error( `Route ${ diff --git a/packages/docusaurus/src/server/translations/translations.ts b/packages/docusaurus/src/server/translations/translations.ts index 91b350b701ef..54cc006cccc5 100644 --- a/packages/docusaurus/src/server/translations/translations.ts +++ b/packages/docusaurus/src/server/translations/translations.ts @@ -113,9 +113,9 @@ async function writeTranslationFileContent({ const existingContent = await readTranslationFileContent(filePath); // Warn about potential legacy keys - const unknownKeys = Object.keys(existingContent ?? {}).filter( - (key) => !Object.hasOwn(newContent, key), - ); + const existingKeys = new Set(Object.keys(existingContent ?? {})); + const newKeys = new Set(Object.keys(newContent)); + const unknownKeys = [...existingKeys.difference(newKeys)]; if (unknownKeys.length > 0) { logger.warn`Some translation keys looks unknown to us in file path=${filePath}. Maybe you should remove them? ${unknownKeys}`; @@ -275,9 +275,11 @@ export function applyDefaultCodeTranslations({ extractedCodeTranslations: TranslationFileContent; defaultCodeMessages: CodeTranslations; }): TranslationFileContent { - const unusedDefaultCodeMessages = Object.keys(defaultCodeMessages).filter( - (messageId) => !Object.hasOwn(extractedCodeTranslations, messageId), - ); + const defaultMessageIds = new Set(Object.keys(defaultCodeMessages)); + const extractedMessageIds = new Set(Object.keys(extractedCodeTranslations)); + const unusedDefaultCodeMessages = [ + ...defaultMessageIds.difference(extractedMessageIds), + ]; if (unusedDefaultCodeMessages.length > 0) { logger.warn`Unused default message codes found. Please report this Docusaurus issue. name=${unusedDefaultCodeMessages}`;