Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Replace lodash with es-toolkit #28981

Draft
wants to merge 8 commits into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions code/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,6 @@
"@types/find-cache-dir": "^5.0.0",
"@types/fs-extra": "^11.0.1",
"@types/js-yaml": "^4.0.5",
"@types/lodash": "^4.14.167",
"@types/node": "^22.0.0",
"@types/npmlog": "^7.0.0",
"@types/picomatch": "^2.3.0",
Expand Down Expand Up @@ -360,6 +359,7 @@
"diff": "^5.2.0",
"downshift": "^9.0.4",
"ejs": "^3.1.10",
"es-toolkit": "^1.16.0",
"esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0",
"esbuild-plugin-alias": "^0.2.1",
"execa": "^8.0.1",
Expand All @@ -380,7 +380,6 @@
"jsdoc-type-pratt-parser": "^4.0.0",
"lazy-universal-dotenv": "^4.0.0",
"leven": "^4.0.0",
"lodash": "^4.17.21",
"markdown-to-jsx": "^7.4.5",
"memfs": "^4.11.1",
"memoizerific": "^1.11.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export abstract class JsPackageManager {
/** Get the INSTALLED version of a package from the package.json file */
async getPackageVersion(packageName: string, basePath = this.cwd): Promise<string | null> {
const packageJSON = await this.getPackageJSON(packageName, basePath);
return packageJSON ? packageJSON.version ?? null : null;
return packageJSON ? (packageJSON.version ?? null) : null;
}

constructor(options?: JsPackageManagerOptions) {
Expand Down
7 changes: 3 additions & 4 deletions code/core/src/core-server/utils/stories-json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

import { STORY_INDEX_INVALIDATED } from '@storybook/core/core-events';

import { debounce } from 'es-toolkit';
import type { Request, Response, Router } from 'express';
import debounce from 'lodash/debounce.js';
import Watchpack from 'watchpack';

import { csfIndexer } from '../presets/common-preset';
Expand All @@ -17,7 +17,7 @@
import { DEBOUNCE, useStoriesJson } from './stories-json';

vi.mock('watchpack');
vi.mock('lodash/debounce');
vi.mock('es-toolkit');
vi.mock('@storybook/core/node-logger');

const workingDir = join(__dirname, '__mockdata__');
Expand Down Expand Up @@ -471,8 +471,7 @@

it('debounces invalidation events', async () => {
vi.mocked(debounce).mockImplementation(
// @ts-expect-error it doesn't think default exists
(await vi.importActual<typeof import('lodash/debounce.js')>('lodash/debounce.js')).default
(await vi.importActual<typeof import('es-toolkit')>('es-toolkit')).debounce
);

const mockServerChannel = { emit: vi.fn() } as any as ServerChannel;
Expand Down Expand Up @@ -509,7 +508,7 @@
await onChange(`${workingDir}/src/nested/Button.stories.ts`);
await onChange(`${workingDir}/src/nested/Button.stories.ts`);

expect(mockServerChannel.emit).toHaveBeenCalledTimes(1);

Check failure on line 511 in code/core/src/core-server/utils/stories-json.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/core-server/utils/stories-json.test.ts > useStoriesJson > SSE endpoint > debounces invalidation events

AssertionError: expected "spy" to be called 1 times, but got 0 times ❯ src/core-server/utils/stories-json.test.ts:511:38
expect(mockServerChannel.emit).toHaveBeenCalledWith(STORY_INDEX_INVALIDATED);

await new Promise((r) => setTimeout(r, 2 * DEBOUNCE));
Expand Down
6 changes: 2 additions & 4 deletions code/core/src/core-server/utils/stories-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import type { NormalizedStoriesSpecifier, StoryIndex } from '@storybook/core/typ

import { STORY_INDEX_INVALIDATED } from '@storybook/core/core-events';

import { debounce } from 'es-toolkit';
import type { Request, Response, Router } from 'express';
import { writeJSON } from 'fs-extra';
import debounce from 'lodash/debounce.js';

import type { StoryIndexGenerator } from './StoryIndexGenerator';
import type { ServerChannel } from './get-server-channel';
Expand Down Expand Up @@ -40,9 +40,7 @@ export function useStoriesJson({
configDir?: string;
normalizedStories: NormalizedStoriesSpecifier[];
}) {
const maybeInvalidate = debounce(() => serverChannel.emit(STORY_INDEX_INVALIDATED), DEBOUNCE, {
leading: true,
});
const maybeInvalidate = debounce(() => serverChannel.emit(STORY_INDEX_INVALIDATED), DEBOUNCE, {});
watchStorySpecifiers(normalizedStories, { workingDir }, async (specifier, path, removed) => {
const generator = await initializedStoryIndexGenerator;
generator.invalidate(specifier, path, removed);
Expand Down
2 changes: 1 addition & 1 deletion code/core/src/docs-tools/argTypes/convert/convert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest';

import { transformSync } from '@storybook/core/babel';

import mapValues from 'lodash/mapValues.js';
import { mapValues } from 'es-toolkit';
import requireFromString from 'require-from-string';

import { normalizeNewlines } from '../utils';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SBType } from '@storybook/core/types';

import mapValues from 'lodash/mapValues.js';
import { mapValues } from 'es-toolkit';

import { parseLiteral } from '../utils';
import type { PTType } from './types';
Expand Down
67 changes: 62 additions & 5 deletions code/core/src/manager-api/lib/merge.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { logger } from '@storybook/core/client-logger';

import isEqual from 'lodash/isEqual.js';
import mergeWith from 'lodash/mergeWith.js';
import { isEqual, mergeWith, omitBy, pick } from 'es-toolkit';

export default <TObj = any>(a: TObj, b: Partial<TObj>) =>
mergeWith({}, a, b, (objValue: TObj, srcValue: Partial<TObj>) => {
export default <TObj = any>(a: TObj, ...b: Partial<TObj>[]): TObj => {
// start with empty object
let target = {};

// merge object a unto target
target = mergeWith({}, a, (objValue: TObj, srcValue: Partial<TObj>) => {
if (Array.isArray(srcValue) && Array.isArray(objValue)) {
srcValue.forEach((s) => {
const existing = objValue.find((o) => o === s || isEqual(o, s));
Expand All @@ -19,5 +22,59 @@ export default <TObj = any>(a: TObj, b: Partial<TObj>) =>
logger.log(['the types mismatch, picking', objValue]);
return objValue;
}
return undefined;
});

for (const obj of b) {
// merge object b unto target
target = mergeWith(target, obj, (objValue: TObj, srcValue: Partial<TObj>) => {
if (Array.isArray(srcValue) && Array.isArray(objValue)) {
srcValue.forEach((s) => {
const existing = objValue.find((o) => o === s || isEqual(o, s));
if (!existing) {
objValue.push(s);
}
});

return objValue;
}
if (Array.isArray(objValue)) {
logger.log(['the types mismatch, picking', objValue]);
return objValue;
}
});
}

return target as TObj;
};

export const noArrayMerge = <TObj = any>(a: TObj, ...b: Partial<TObj>[]): TObj => {
// start with empty object
let target = {};

// merge object a unto target
target = mergeWith({}, a, (objValue: TObj, srcValue: Partial<TObj>) => {
// Treat arrays as scalars:
if (Array.isArray(srcValue)) {
return srcValue;
}
});

for (const obj of b) {
// merge object b unto target
target = mergeWith(target, obj, (objValue: TObj, srcValue: Partial<TObj>) => {
// Treat arrays as scalars:
if (Array.isArray(srcValue)) {
return srcValue;
}
});
}

return target as TObj;
};

export function picky<T extends Record<string, any>, K extends keyof T>(
obj: T,
keys: K[]
): Partial<Pick<T, K>> {
return omitBy(pick(obj, keys), (v) => v === undefined);
}
7 changes: 3 additions & 4 deletions code/core/src/manager-api/lib/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import type {
} from '@storybook/core/types';
import { sanitize } from '@storybook/csf';

import countBy from 'lodash/countBy.js';
import mapValues from 'lodash/mapValues.js';
import { countBy, mapValues } from 'es-toolkit';
import memoize from 'memoizerific';
import { dedent } from 'ts-dedent';

Expand All @@ -41,7 +40,7 @@ export const denormalizeStoryParameters = ({
kindParameters[storyData.kind],
storyData.parameters as unknown as Parameters
),
}));
})) as SetStoriesStoryData;
};

export const transformSetStoriesStoryDataToStoriesHash = (
Expand Down Expand Up @@ -112,7 +111,7 @@ export const transformStoryIndexV2toV3 = (index: StoryIndexV2): StoryIndexV3 =>
};

export const transformStoryIndexV3toV4 = (index: StoryIndexV3): API_PreparedStoryIndex => {
const countByTitle = countBy(Object.values(index.stories), 'title');
const countByTitle = countBy(Object.values(index.stories), (item) => item.title);
return {
v: 4,
entries: Object.values(index.stories).reduce(
Expand Down
20 changes: 11 additions & 9 deletions code/core/src/manager-api/modules/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import { global } from '@storybook/global';

import { SET_CONFIG } from '@storybook/core/core-events';

import { dequal as deepEqual } from 'dequal';
import pick from 'lodash/pick.js';
import { isEqual as deepEqual, omitBy, pick, toMerged } from 'es-toolkit';

import merge from '../lib/merge';
import merge, { picky } from '../lib/merge';
import type { ModuleFn } from '../lib/types';
import type { State } from '../root';

Expand Down Expand Up @@ -320,12 +319,15 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, provider, singleStory
...defaultLayoutState,
layout: {
...defaultLayoutState.layout,
...pick(options, Object.keys(defaultLayoutState.layout)),
...toMerged(
defaultLayoutState.layout,
pick(options, Object.keys(defaultLayoutState.layout))
),
...(singleStory && { navSize: 0 }),
},
ui: {
...defaultLayoutState.ui,
...pick(options, Object.keys(defaultLayoutState.ui)),
...toMerged(defaultLayoutState.ui, pick(options, Object.keys(defaultLayoutState.ui))),
},
selectedPanel: selectedPanel || defaultLayoutState.selectedPanel,
theme: theme || defaultLayoutState.theme,
Expand All @@ -351,15 +353,15 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, provider, singleStory

const updatedLayout = {
...layout,
...options.layout,
...pick(options, Object.keys(layout)),
...(options.layout || {}),
...picky(options, Object.keys(layout)),
...(singleStory && { navSize: 0 }),
};

const updatedUi = {
...ui,
...options.ui,
...pick(options, Object.keys(ui)),
...toMerged(options.ui || {}, picky(options, Object.keys(ui))),
};

const updatedTheme = {
Expand Down Expand Up @@ -388,7 +390,7 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, provider, singleStory
},
};

const persisted = pick(store.getState(), 'layout', 'selectedPanel');
const persisted = pick(store.getState(), ['layout', 'selectedPanel']);

provider.channel?.on(SET_CONFIG, () => {
api.setOptions(merge(api.getInitialOptions(), persisted));
Expand Down
2 changes: 1 addition & 1 deletion code/core/src/manager-api/modules/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { API_Notification } from '@storybook/core/types';

import partition from 'lodash/partition.js';
import { partition } from 'es-toolkit';

import type { ModuleFn } from '../lib/types';

Expand Down
12 changes: 3 additions & 9 deletions code/core/src/manager-api/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ import {
STORY_CHANGED,
} from '@storybook/core/core-events';

import mergeWith from 'lodash/mergeWith.js';
import { mergeWith } from 'es-toolkit';

import { createContext } from './context';
import getInitialState from './initial-state';
import { types } from './lib/addons';
import { noArrayMerge } from './lib/merge';
import type { ModuleFn } from './lib/types';
import * as addons from './modules/addons';
import * as channel from './modules/channel';
Expand Down Expand Up @@ -129,14 +130,7 @@ export type ManagerProviderProps = RouterData &

// This is duplicated from @storybook/preview-api for the reasons mentioned in lib-addons/types.js
export const combineParameters = (...parameterSets: Parameters[]) =>
mergeWith({}, ...parameterSets, (objValue: any, srcValue: any) => {
// Treat arrays as scalars:
if (Array.isArray(srcValue)) {
return srcValue;
}

return undefined;
});
noArrayMerge({}, ...parameterSets);

class ManagerProvider extends Component<ManagerProviderProps, State> {
api: API = {} as API;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { within } from '@storybook/test';

import { ManagerContext } from '@storybook/core/manager-api';

import { startCase } from 'lodash';
import { startCase } from 'es-toolkit';

import { LayoutProvider, useLayout } from '../../layout/LayoutProvider';
import { MobileNavigation } from './MobileNavigation';
Expand Down
2 changes: 1 addition & 1 deletion code/core/src/manager/components/preview/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export function filterTabs(panels: Addon_BaseType[], parameters?: Record<string,

if (previewTabs || parametersTabs) {
// deep merge global and local settings
const tabs = merge(previewTabs, parametersTabs);
const tabs = merge(previewTabs || {}, parametersTabs || {});
const arrTabs = Object.keys(tabs).map((key, index) => ({
index,
...(typeof tabs[key] === 'string' ? { title: tabs[key] } : tabs[key]),
Expand Down
5 changes: 4 additions & 1 deletion code/core/src/manager/components/preview/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,10 @@ function toolbarItemHasBeenExcluded(item: Partial<Addon_BaseType>, entry: LeafEn
const toolbarItemsFromStoryParameters = 'toolbar' in parameters ? parameters.toolbar : undefined;
const { toolbar: toolbarItemsFromAddonsConfig } = addons.getConfig();

const toolbarItems = merge(toolbarItemsFromAddonsConfig, toolbarItemsFromStoryParameters);
const toolbarItems = merge(
toolbarItemsFromAddonsConfig || {},
toolbarItemsFromStoryParameters || {}
);

// @ts-expect-error (non strict)
return toolbarItems ? !!toolbarItems[item?.id]?.hidden : false;
Expand Down
2 changes: 1 addition & 1 deletion code/core/src/manager/components/sidebar/useExpanded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { STORIES_COLLAPSE_ALL, STORIES_EXPAND_ALL } from '@storybook/core/core-e
import type { StoriesHash } from '@storybook/core/manager-api';
import { useStorybookApi } from '@storybook/core/manager-api';

import throttle from 'lodash/throttle.js';
import { throttle } from 'es-toolkit';

import { matchesKeyCode, matchesModifiers } from '../../keybinding';
import { getAncestorIds, getDescendantIds, isAncestor, scrollIntoView } from '../../utils/tree';
Expand Down
2 changes: 1 addition & 1 deletion code/core/src/manager/components/sidebar/useLastViewed.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';

import debounce from 'lodash/debounce.js';
import { debounce } from 'es-toolkit';
import store from 'store2';

import type { Selection, StoryRef } from './types';
Expand Down
10 changes: 5 additions & 5 deletions code/core/src/preview-api/modules/preview-web/PreviewWeb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
UPDATE_STORY_ARGS,
} from '@storybook/core/core-events';

import merge from 'lodash/merge.js';
import { merge, toMerged } from 'es-toolkit';

import { addons } from '../addons';
import type { StoryStore } from '../store';
Expand Down Expand Up @@ -308,7 +308,7 @@ describe('PreviewWeb', () => {
expect(mockChannel.emit).toHaveBeenCalledWith(STORY_MISSING, 'component-one--missing');

mockChannel.emit.mockClear();
const newComponentOneExports = merge({}, componentOneExports, {
const newComponentOneExports = toMerged(componentOneExports, {
d: { args: { foo: 'd' }, play: vi.fn() },
});
const newImportFn = vi.fn(async (path) => {
Expand Down Expand Up @@ -362,7 +362,7 @@ describe('PreviewWeb', () => {
});
await waitForSetCurrentStory();

const newComponentOneExports = merge({}, componentOneExports, {
const newComponentOneExports = toMerged(componentOneExports, {
d: { args: { foo: 'd' }, play: vi.fn() },
});
const newImportFn = vi.fn(async (path) => {
Expand Down Expand Up @@ -2927,7 +2927,7 @@ describe('PreviewWeb', () => {
});

describe('when the current story changes', () => {
const newComponentOneExports = merge({}, componentOneExports, {
const newComponentOneExports = toMerged(componentOneExports, {
a: { args: { foo: 'edited' } },
});
const newImportFn = vi.fn(async (path) => {
Expand Down Expand Up @@ -3282,7 +3282,7 @@ describe('PreviewWeb', () => {
afterEach(() => {
vi.useRealTimers();
});
const newComponentOneExports = merge({}, componentOneExports, {
const newComponentOneExports = toMerged(componentOneExports, {
a: { args: { bar: 'edited' }, argTypes: { bar: { type: { name: 'string' } } } },
});
const newImportFn = vi.fn(async (path) => {
Expand Down
Loading
Loading