From b14cbbdce930a1204e290792ae650cacfc2228ec Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 2 Apr 2026 12:39:08 +0200 Subject: [PATCH 1/6] Normalize file paths in ChangeDetectionService and trace-changed for Windows support --- .../ChangeDetectionService.ts | 8 ++--- .../change-detection/trace-changed.ts | 34 +++++++++++++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/code/core/src/core-server/change-detection/ChangeDetectionService.ts b/code/core/src/core-server/change-detection/ChangeDetectionService.ts index 84ae722cc570..1fb4b8a5ec2d 100644 --- a/code/core/src/core-server/change-detection/ChangeDetectionService.ts +++ b/code/core/src/core-server/change-detection/ChangeDetectionService.ts @@ -10,6 +10,7 @@ import type { } from 'storybook/internal/types'; import { CHANGE_DETECTION_STATUS_TYPE_ID } from 'storybook/internal/types'; +import { normalizePath } from '../../common/utils/normalize-path.ts'; import type { StoryIndexGenerator } from '../utils/StoryIndexGenerator.ts'; import { ChangeDetectionFailureError, ChangeDetectionUnavailableError } from './errors.ts'; import { GitDiffProvider } from './GitDiffProvider.ts'; @@ -45,8 +46,7 @@ function getStoryIdsByAbsolutePath( return; } - const absolutePath = resolve(workingDir, entry.importPath); - // logger.info(`Story ${entry.id} absolute path: ${absolutePath}`); + const absolutePath = normalizePath(resolve(workingDir, entry.importPath)); const storyIds = storyIdsByFile.get(absolutePath) ?? new Set(); storyIds.add(entry.id); storyIdsByFile.set(absolutePath, storyIds); @@ -244,10 +244,10 @@ export class ChangeDetectionService { ]); const changedFiles = new Set( - Array.from(changes.changed).map((filePath) => resolve(repoRoot, filePath)) + Array.from(changes.changed).map((filePath) => normalizePath(resolve(repoRoot, filePath))) ); const newFiles = new Set( - Array.from(changes.new).map((filePath) => resolve(repoRoot, filePath)) + Array.from(changes.new).map((filePath) => normalizePath(resolve(repoRoot, filePath))) ); const scannedFiles = new Set([...changedFiles, ...newFiles]); diff --git a/code/core/src/core-server/change-detection/trace-changed.ts b/code/core/src/core-server/change-detection/trace-changed.ts index 697badcc69ac..14326b9d5b57 100644 --- a/code/core/src/core-server/change-detection/trace-changed.ts +++ b/code/core/src/core-server/change-detection/trace-changed.ts @@ -1,17 +1,38 @@ import type { ModuleGraph, ModuleNode } from 'storybook/internal/types'; +import { normalizePath } from '../../common/utils/normalize-path.ts'; + +function getModuleNodesByNormalizedPath( + moduleGraph: ModuleGraph, + normalizedPath: string +): Set | undefined { + const directMatch = moduleGraph.get(normalizedPath); + if (directMatch) { + return directMatch; + } + + for (const [modulePath, nodes] of moduleGraph.entries()) { + if (normalizePath(modulePath) === normalizedPath) { + return nodes; + } + } + + return undefined; +} + export function findAffectedStoryFiles( changedFile: string, moduleGraph: ModuleGraph, storyIdsByFile: Map> ): Map { const affectedStoryFiles = new Map(); + const normalizedChangedFile = normalizePath(changedFile); - if (storyIdsByFile.has(changedFile)) { - affectedStoryFiles.set(changedFile, { distance: 0 }); + if (storyIdsByFile.has(normalizedChangedFile)) { + affectedStoryFiles.set(normalizedChangedFile, { distance: 0 }); } - const startingNodes = moduleGraph.get(changedFile); + const startingNodes = getModuleNodesByNormalizedPath(moduleGraph, normalizedChangedFile); if (!startingNodes?.size) { return affectedStoryFiles; } @@ -35,11 +56,12 @@ export function findAffectedStoryFiles( } visited.set(importer, distance); - if (storyIdsByFile.has(importer.file)) { - const previousStoryDistance = affectedStoryFiles.get(importer.file)?.distance; + const normalizedImporterFile = normalizePath(importer.file); + if (storyIdsByFile.has(normalizedImporterFile)) { + const previousStoryDistance = affectedStoryFiles.get(normalizedImporterFile)?.distance; if (previousStoryDistance === undefined || distance < previousStoryDistance) { - affectedStoryFiles.set(importer.file, { distance }); + affectedStoryFiles.set(normalizedImporterFile, { distance }); } } From f814fc7e0d906009c350a671110b0fecbd96e150 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 2 Apr 2026 13:41:51 +0200 Subject: [PATCH 2/6] Use 'join' for path resolution and remove unnecessary normalization --- .../ChangeDetectionService.ts | 8 ++--- .../change-detection/trace-changed.ts | 34 ++++--------------- 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/code/core/src/core-server/change-detection/ChangeDetectionService.ts b/code/core/src/core-server/change-detection/ChangeDetectionService.ts index 1fb4b8a5ec2d..695173f82dd0 100644 --- a/code/core/src/core-server/change-detection/ChangeDetectionService.ts +++ b/code/core/src/core-server/change-detection/ChangeDetectionService.ts @@ -1,4 +1,4 @@ -import { relative, resolve } from 'node:path'; +import { join, relative } from 'node:path'; import { logger } from 'storybook/internal/node-logger'; import type { @@ -46,7 +46,7 @@ function getStoryIdsByAbsolutePath( return; } - const absolutePath = normalizePath(resolve(workingDir, entry.importPath)); + const absolutePath = normalizePath(join(workingDir, entry.importPath)); const storyIds = storyIdsByFile.get(absolutePath) ?? new Set(); storyIds.add(entry.id); storyIdsByFile.set(absolutePath, storyIds); @@ -244,10 +244,10 @@ export class ChangeDetectionService { ]); const changedFiles = new Set( - Array.from(changes.changed).map((filePath) => normalizePath(resolve(repoRoot, filePath))) + Array.from(changes.changed).map((filePath) => normalizePath(join(repoRoot, filePath))) ); const newFiles = new Set( - Array.from(changes.new).map((filePath) => normalizePath(resolve(repoRoot, filePath))) + Array.from(changes.new).map((filePath) => normalizePath(join(repoRoot, filePath))) ); const scannedFiles = new Set([...changedFiles, ...newFiles]); diff --git a/code/core/src/core-server/change-detection/trace-changed.ts b/code/core/src/core-server/change-detection/trace-changed.ts index 14326b9d5b57..697badcc69ac 100644 --- a/code/core/src/core-server/change-detection/trace-changed.ts +++ b/code/core/src/core-server/change-detection/trace-changed.ts @@ -1,38 +1,17 @@ import type { ModuleGraph, ModuleNode } from 'storybook/internal/types'; -import { normalizePath } from '../../common/utils/normalize-path.ts'; - -function getModuleNodesByNormalizedPath( - moduleGraph: ModuleGraph, - normalizedPath: string -): Set | undefined { - const directMatch = moduleGraph.get(normalizedPath); - if (directMatch) { - return directMatch; - } - - for (const [modulePath, nodes] of moduleGraph.entries()) { - if (normalizePath(modulePath) === normalizedPath) { - return nodes; - } - } - - return undefined; -} - export function findAffectedStoryFiles( changedFile: string, moduleGraph: ModuleGraph, storyIdsByFile: Map> ): Map { const affectedStoryFiles = new Map(); - const normalizedChangedFile = normalizePath(changedFile); - if (storyIdsByFile.has(normalizedChangedFile)) { - affectedStoryFiles.set(normalizedChangedFile, { distance: 0 }); + if (storyIdsByFile.has(changedFile)) { + affectedStoryFiles.set(changedFile, { distance: 0 }); } - const startingNodes = getModuleNodesByNormalizedPath(moduleGraph, normalizedChangedFile); + const startingNodes = moduleGraph.get(changedFile); if (!startingNodes?.size) { return affectedStoryFiles; } @@ -56,12 +35,11 @@ export function findAffectedStoryFiles( } visited.set(importer, distance); - const normalizedImporterFile = normalizePath(importer.file); - if (storyIdsByFile.has(normalizedImporterFile)) { - const previousStoryDistance = affectedStoryFiles.get(normalizedImporterFile)?.distance; + if (storyIdsByFile.has(importer.file)) { + const previousStoryDistance = affectedStoryFiles.get(importer.file)?.distance; if (previousStoryDistance === undefined || distance < previousStoryDistance) { - affectedStoryFiles.set(normalizedImporterFile, { distance }); + affectedStoryFiles.set(importer.file, { distance }); } } From 7e699f6e73e8ce89fd885e50ed02ff946477073d Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 2 Apr 2026 15:07:21 +0200 Subject: [PATCH 3/6] Refactor ChangeDetectionService tests to use normalized paths for module nodes --- .../ChangeDetectionService.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/code/core/src/core-server/change-detection/ChangeDetectionService.test.ts b/code/core/src/core-server/change-detection/ChangeDetectionService.test.ts index 1ad7d0d269d8..c07df01975a9 100644 --- a/code/core/src/core-server/change-detection/ChangeDetectionService.test.ts +++ b/code/core/src/core-server/change-detection/ChangeDetectionService.test.ts @@ -12,6 +12,7 @@ import type { } from 'storybook/internal/types'; import { CHANGE_DETECTION_STATUS_TYPE_ID } from 'storybook/internal/types'; +import { normalizePath } from '../../common/utils/normalize-path.ts'; import { createStatusStore, UNIVERSAL_STATUS_STORE_OPTIONS, @@ -533,17 +534,20 @@ describe('ChangeDetectionService', () => { }); it('stores changed files as normalized repo-relative paths', async () => { - const buttonCss = createModuleNode(join(workingDir, 'src', 'Button.module.css')); - const buttonComponent = createModuleNode(join(workingDir, 'src', 'Button.tsx')); - const buttonStory = createModuleNode(join(workingDir, 'src', 'Button.stories.tsx')); + const buttonCssPath = normalizePath(join(workingDir, 'src', 'Button.module.css')); + const buttonComponentPath = normalizePath(join(workingDir, 'src', 'Button.tsx')); + const buttonStoryPath = normalizePath(join(workingDir, 'src', 'Button.stories.tsx')); + const buttonCss = createModuleNode(buttonCssPath); + const buttonComponent = createModuleNode(buttonComponentPath); + const buttonStory = createModuleNode(buttonStoryPath); buttonCss.importers.add(buttonComponent); buttonComponent.importers.add(buttonStory); const moduleGraph: ModuleGraph = new Map([ - [join(workingDir, 'src', 'Button.module.css'), new Set([buttonCss])], - [join(workingDir, 'src', 'Button.tsx'), new Set([buttonComponent])], - [join(workingDir, 'src', 'Button.stories.tsx'), new Set([buttonStory])], + [buttonCssPath, new Set([buttonCss])], + [buttonComponentPath, new Set([buttonComponent])], + [buttonStoryPath, new Set([buttonStory])], ]); const storyIndex = createStoryIndex([ { storyId: 'button--primary', importPath: './src/Button.stories.tsx', title: 'Button' }, From 1f509859a4bde087e3a04df35ec09c4f3a02bb3f Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 2 Apr 2026 15:15:35 +0200 Subject: [PATCH 4/6] Refactor ChangeDetectionService to use 'pathe' for path resolution and remove unnecessary normalization --- .../ChangeDetectionService.test.ts | 9 ++++----- .../change-detection/ChangeDetectionService.ts | 18 +++++------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/code/core/src/core-server/change-detection/ChangeDetectionService.test.ts b/code/core/src/core-server/change-detection/ChangeDetectionService.test.ts index c07df01975a9..0fdcac101331 100644 --- a/code/core/src/core-server/change-detection/ChangeDetectionService.test.ts +++ b/code/core/src/core-server/change-detection/ChangeDetectionService.test.ts @@ -1,4 +1,4 @@ -import { join } from 'node:path'; +import { join } from 'pathe'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -12,7 +12,6 @@ import type { } from 'storybook/internal/types'; import { CHANGE_DETECTION_STATUS_TYPE_ID } from 'storybook/internal/types'; -import { normalizePath } from '../../common/utils/normalize-path.ts'; import { createStatusStore, UNIVERSAL_STATUS_STORE_OPTIONS, @@ -534,9 +533,9 @@ describe('ChangeDetectionService', () => { }); it('stores changed files as normalized repo-relative paths', async () => { - const buttonCssPath = normalizePath(join(workingDir, 'src', 'Button.module.css')); - const buttonComponentPath = normalizePath(join(workingDir, 'src', 'Button.tsx')); - const buttonStoryPath = normalizePath(join(workingDir, 'src', 'Button.stories.tsx')); + const buttonCssPath = join(workingDir, 'src', 'Button.module.css'); + const buttonComponentPath = join(workingDir, 'src', 'Button.tsx'); + const buttonStoryPath = join(workingDir, 'src', 'Button.stories.tsx'); const buttonCss = createModuleNode(buttonCssPath); const buttonComponent = createModuleNode(buttonComponentPath); const buttonStory = createModuleNode(buttonStoryPath); diff --git a/code/core/src/core-server/change-detection/ChangeDetectionService.ts b/code/core/src/core-server/change-detection/ChangeDetectionService.ts index 695173f82dd0..7c0efa8343ab 100644 --- a/code/core/src/core-server/change-detection/ChangeDetectionService.ts +++ b/code/core/src/core-server/change-detection/ChangeDetectionService.ts @@ -1,4 +1,4 @@ -import { join, relative } from 'node:path'; +import { join, relative } from 'pathe'; import { logger } from 'storybook/internal/node-logger'; import type { @@ -10,7 +10,6 @@ import type { } from 'storybook/internal/types'; import { CHANGE_DETECTION_STATUS_TYPE_ID } from 'storybook/internal/types'; -import { normalizePath } from '../../common/utils/normalize-path.ts'; import type { StoryIndexGenerator } from '../utils/StoryIndexGenerator.ts'; import { ChangeDetectionFailureError, ChangeDetectionUnavailableError } from './errors.ts'; import { GitDiffProvider } from './GitDiffProvider.ts'; @@ -46,7 +45,7 @@ function getStoryIdsByAbsolutePath( return; } - const absolutePath = normalizePath(join(workingDir, entry.importPath)); + const absolutePath = join(workingDir, entry.importPath); const storyIds = storyIdsByFile.get(absolutePath) ?? new Set(); storyIds.add(entry.id); storyIdsByFile.set(absolutePath, storyIds); @@ -74,11 +73,6 @@ function mergeStatusValues( return nextValue; } -function toRepoRelativePath(repoRoot: string, filePath: string): string { - const relativePath = relative(repoRoot, filePath); - return relativePath.startsWith('\\\\?\\') ? relativePath : relativePath.replace(/\\/g, '/'); -} - /** * Coordinates change detection by listening to builder module-graph updates, resolving changed * files from git, mapping those changes to affected stories, and publishing the resulting story @@ -244,11 +238,9 @@ export class ChangeDetectionService { ]); const changedFiles = new Set( - Array.from(changes.changed).map((filePath) => normalizePath(join(repoRoot, filePath))) - ); - const newFiles = new Set( - Array.from(changes.new).map((filePath) => normalizePath(join(repoRoot, filePath))) + Array.from(changes.changed).map((filePath) => join(repoRoot, filePath)) ); + const newFiles = new Set(Array.from(changes.new).map((filePath) => join(repoRoot, filePath))); const scannedFiles = new Set([...changedFiles, ...newFiles]); const storyIndex = await storyIndexGenerator.getIndex(); @@ -276,7 +268,7 @@ export class ChangeDetectionService { storyIds.forEach((storyId) => { const existingStatus = statuses.get(storyId); const changedStoryFiles = new Set(existingStatus?.data?.changedFiles ?? []); - changedStoryFiles.add(toRepoRelativePath(repoRoot, changedFile)); + changedStoryFiles.add(relative(repoRoot, changedFile)); statuses.set(storyId, { storyId, From 0fdce35ac1d02598ba0b5cf00c03906d698bb412 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 2 Apr 2026 16:56:31 +0200 Subject: [PATCH 5/6] Fix merge conflicts --- .../ChangeDetectionService.ts | 39 ++++--------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/code/core/src/core-server/change-detection/ChangeDetectionService.ts b/code/core/src/core-server/change-detection/ChangeDetectionService.ts index a642113a38f4..c799b51f9fdd 100644 --- a/code/core/src/core-server/change-detection/ChangeDetectionService.ts +++ b/code/core/src/core-server/change-detection/ChangeDetectionService.ts @@ -41,30 +41,14 @@ function getStoryIdsByAbsolutePath( workingDir: string ): Map> { const storyIdsByFile = new Map>(); - const addStoryId = (filePath: string, storyId: string) => { - const storyIds = storyIdsByFile.get(filePath) ?? new Set(); - storyIds.add(storyId); - storyIdsByFile.set(filePath, storyIds); - }; - Object.values(storyIndex.entries).forEach((entry) => { - if (entry.type !== 'story' || entry.importPath.startsWith('virtual:')) { - return; + if (entry.type === 'story' && !entry.importPath.startsWith('virtual:')) { + const filePath = join(workingDir, entry.importPath); + const storyIds = storyIdsByFile.get(filePath) ?? new Set(); + storyIds.add(entry.id); + storyIdsByFile.set(filePath, storyIds); } - -<<<<<<< change-detection-windows - const absolutePath = join(workingDir, entry.importPath); - const storyIds = storyIdsByFile.get(absolutePath) ?? new Set(); - storyIds.add(entry.id); - storyIdsByFile.set(absolutePath, storyIds); -======= - const absolutePath = resolve(workingDir, entry.importPath); - const normalizedAbsolutePath = normalizePath(absolutePath); - addStoryId(absolutePath, entry.id); - addStoryId(normalizedAbsolutePath, entry.id); ->>>>>>> next }); - return storyIdsByFile; } @@ -257,17 +241,8 @@ export class ChangeDetectionService { this.options.storyIndexGeneratorPromise, ]); - const changedFiles = new Set( -<<<<<<< change-detection-windows - Array.from(changes.changed).map((filePath) => join(repoRoot, filePath)) -======= - Array.from(changes.changed).map((filePath) => normalizePath(resolve(repoRoot, filePath))) - ); - const newFiles = new Set( - Array.from(changes.new).map((filePath) => normalizePath(resolve(repoRoot, filePath))) ->>>>>>> next - ); - const newFiles = new Set(Array.from(changes.new).map((filePath) => join(repoRoot, filePath))); + const changedFiles = new Set(Array.from(changes.changed).map((path) => join(repoRoot, path))); + const newFiles = new Set(Array.from(changes.new).map((path) => join(repoRoot, path))); const scannedFiles = new Set([...changedFiles, ...newFiles]); const normalizedModuleGraph = new Map>(); moduleGraph.forEach((nodes, filePath) => { From b306402b9a0f5892465913bebfe8b73b4c297fd2 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 2 Apr 2026 17:24:22 +0200 Subject: [PATCH 6/6] Update GitDiffProvider to use 'pathe' for path resolution --- code/core/src/core-server/change-detection/GitDiffProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/core/src/core-server/change-detection/GitDiffProvider.ts b/code/core/src/core-server/change-detection/GitDiffProvider.ts index 6b522d039994..120cf57078b9 100644 --- a/code/core/src/core-server/change-detection/GitDiffProvider.ts +++ b/code/core/src/core-server/change-detection/GitDiffProvider.ts @@ -1,6 +1,6 @@ import { watch, type FSWatcher } from 'node:fs'; import { readFile, stat } from 'node:fs/promises'; -import { dirname, join, resolve as resolvePath } from 'node:path'; +import { dirname, join, resolve as resolvePath } from 'pathe'; // eslint-disable-next-line depend/ban-dependencies import { execa, type ExecaError } from 'execa';