From fc65414322fb446d2e5a80532071b2b48e5848a9 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Thu, 5 Feb 2026 15:01:00 +0100 Subject: [PATCH] ignore empty story files when indexing --- code/.eslintignore | 1 + .../src/core-server/presets/common-preset.ts | 11 +++- .../utils/StoryIndexGenerator.test.ts | 61 ++++++++++++------- .../utils/__mockdata__/src/Empty.stories.ts | 0 4 files changed, 50 insertions(+), 23 deletions(-) create mode 100644 code/core/src/core-server/utils/__mockdata__/src/Empty.stories.ts diff --git a/code/.eslintignore b/code/.eslintignore index 5efb1ab25edc..0ee7fd976934 100644 --- a/code/.eslintignore +++ b/code/.eslintignore @@ -19,4 +19,5 @@ ember-output !.storybook core/assets core/src/core-server/utils/__search-files-tests__ +core/src/core-server/utils/__mockdata__/src/Empty.stories.ts core/report diff --git a/code/core/src/core-server/presets/common-preset.ts b/code/core/src/core-server/presets/common-preset.ts index 36f40b823c94..cd9ab70adc9d 100644 --- a/code/core/src/core-server/presets/common-preset.ts +++ b/code/core/src/core-server/presets/common-preset.ts @@ -13,7 +13,7 @@ import { removeAddon as removeAddonBase, } from 'storybook/internal/common'; import { StoryIndexGenerator } from 'storybook/internal/core-server'; -import { readCsf } from 'storybook/internal/csf-tools'; +import { loadCsf } from 'storybook/internal/csf-tools'; import { logger } from 'storybook/internal/node-logger'; import { telemetry } from 'storybook/internal/telemetry'; import type { @@ -215,7 +215,14 @@ export const features: PresetProperty<'features'> = async (existing) => ({ export const csfIndexer: Indexer = { test: /(stories|story)\.(m?js|ts)x?$/, - createIndex: async (fileName, options) => (await readCsf(fileName, options)).parse().indexInputs, + createIndex: async (fileName, options) => { + const code = (await readFile(fileName, 'utf-8')).toString(); + if (code.trim().length === 0) { + logger.debug(`The file ${fileName} is empty. Skipping indexing.`); + return []; + } + return loadCsf(code, { ...options, fileName }).parse().indexInputs; + }, }; export const experimental_indexers: PresetProperty<'experimental_indexers'> = (existingIndexers) => diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts index 857f06b12542..6692ea1881b4 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { normalizeStoriesEntry } from 'storybook/internal/common'; import { toId } from 'storybook/internal/csf'; -import { getStorySortParameter, readCsf } from 'storybook/internal/csf-tools'; +import { getStorySortParameter, loadCsf } from 'storybook/internal/csf-tools'; import { logger, once } from 'storybook/internal/node-logger'; import type { NormalizedStoriesSpecifier, StoryIndexEntry } from 'storybook/internal/types'; @@ -34,13 +34,13 @@ vi.mock('storybook/internal/csf-tools', async (importOriginal) => { const csfTools = await importOriginal(); return { ...csfTools, - readCsf: vi.fn(csfTools.readCsf), + loadCsf: vi.fn(csfTools.loadCsf), getStorySortParameter: vi.fn(csfTools.getStorySortParameter), }; }); const toIdMock = vi.mocked(toId); -const readCsfMock = vi.mocked(readCsf); +const loadCsfMock = vi.mocked(loadCsf); const getStorySortParameterMock = vi.mocked(getStorySortParameter); const options: StoryIndexGeneratorOptions = { @@ -55,7 +55,7 @@ describe('StoryIndexGenerator', () => { vi.mocked(logger.warn).mockClear(); vi.mocked(once.warn).mockClear(); toIdMock.mockClear(); - readCsfMock.mockClear(); + loadCsfMock.mockClear(); getStorySortParameterMock.mockClear(); StoryIndexGenerator.clearFindMatchingFilesCache(); }); @@ -221,6 +221,25 @@ describe('StoryIndexGenerator', () => { `); }); }); + describe('empty or whitespace-only files', () => { + it('ignores story files that only contain whitespace (e.g. just a newline)', async () => { + const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( + './src/Empty.stories.ts', + options + ); + + const generator = new StoryIndexGenerator([specifier], options); + await generator.initialize(); + + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` + { + "entries": {}, + "v": 5, + } + `); + }); + }); describe('non-recursive specifier', () => { it('extracts stories from the right files', async () => { const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( @@ -2135,16 +2154,16 @@ describe('StoryIndexGenerator', () => { options ); - readCsfMock.mockClear(); - expect(readCsfMock).toHaveBeenCalledTimes(0); + loadCsfMock.mockClear(); + expect(loadCsfMock).toHaveBeenCalledTimes(0); const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(12); + expect(loadCsfMock).toHaveBeenCalledTimes(12); - readCsfMock.mockClear(); + loadCsfMock.mockClear(); await generator.getIndex(); - expect(readCsfMock).not.toHaveBeenCalled(); + expect(loadCsfMock).not.toHaveBeenCalled(); }); it('does not extract docs files a second time', async () => { @@ -2156,8 +2175,8 @@ describe('StoryIndexGenerator', () => { './src/docs2/*.mdx', options ); - readCsfMock.mockClear(); - expect(readCsfMock).toHaveBeenCalledTimes(0); + loadCsfMock.mockClear(); + expect(loadCsfMock).toHaveBeenCalledTimes(0); const generator = new StoryIndexGenerator([storiesSpecifier, docsSpecifier], options); await generator.initialize(); await generator.getIndex(); @@ -2194,17 +2213,17 @@ describe('StoryIndexGenerator', () => { options ); - readCsfMock.mockClear(); + loadCsfMock.mockClear(); const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(12); + expect(loadCsfMock).toHaveBeenCalledTimes(12); generator.invalidate('./src/B.stories.ts', false); - readCsfMock.mockClear(); + loadCsfMock.mockClear(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(1); + expect(loadCsfMock).toHaveBeenCalledTimes(1); }); it('calls extract docs file for just the one file', async () => { @@ -2279,17 +2298,17 @@ describe('StoryIndexGenerator', () => { options ); - readCsfMock.mockClear(); + loadCsfMock.mockClear(); const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(12); + expect(loadCsfMock).toHaveBeenCalledTimes(12); generator.invalidate('./src/B.stories.ts', true); - readCsfMock.mockClear(); + loadCsfMock.mockClear(); await generator.getIndex(); - expect(readCsfMock).not.toHaveBeenCalled(); + expect(loadCsfMock).not.toHaveBeenCalled(); }); it('does call the sort function a second time', async () => { @@ -2318,11 +2337,11 @@ describe('StoryIndexGenerator', () => { options ); - readCsfMock.mockClear(); + loadCsfMock.mockClear(); const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(12); + expect(loadCsfMock).toHaveBeenCalledTimes(12); generator.invalidate('./src/B.stories.ts', true); diff --git a/code/core/src/core-server/utils/__mockdata__/src/Empty.stories.ts b/code/core/src/core-server/utils/__mockdata__/src/Empty.stories.ts new file mode 100644 index 000000000000..e69de29bb2d1