Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';

import { escapeGlobPath } from './storybook-optimize-deps-plugin';

describe('escapeGlobPath', () => {
it('should not modify a plain path without special characters', () => {
expect(escapeGlobPath('./src/Button.stories.tsx')).toBe('./src/Button.stories.tsx');
});

it('should escape parentheses in path segments (e.g. Next.js route groups)', () => {
expect(escapeGlobPath('./src/(group)/Button.stories.tsx')).toBe(
'./src/\\(group\\)/Button.stories.tsx'
);
});

it('should escape square brackets in path segments', () => {
expect(escapeGlobPath('./src/[id]/Button.stories.tsx')).toBe(
'./src/\\[id\\]/Button.stories.tsx'
);
});

it('should escape curly braces in path segments', () => {
expect(escapeGlobPath('./src/{group}/Button.stories.tsx')).toBe(
'./src/\\{group\\}/Button.stories.tsx'
);
});

it('should escape glob wildcard characters', () => {
expect(escapeGlobPath('./src/Button*.stories.tsx')).toBe('./src/Button\\*.stories.tsx');
expect(escapeGlobPath('./src/Button?.stories.tsx')).toBe('./src/Button\\?.stories.tsx');
});

it('should escape all special glob characters together', () => {
expect(escapeGlobPath('./src/(group)/[id]/{name}/*.stories.tsx')).toBe(
'./src/\\(group\\)/\\[id\\]/\\{name\\}/\\*.stories.tsx'
);
});

it('should not modify paths that contain no special glob characters', () => {
expect(escapeGlobPath('./src/my-component/Button.stories.tsx')).toBe(
'./src/my-component/Button.stories.tsx'
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import { type Plugin } from 'vite';
import { processPreviewAnnotation } from '../utils/process-preview-annotation';
import { getUniqueImportPaths } from '../utils/unique-import-paths';

/**
* Escapes special glob characters in a file path so Vite's dep optimizer treats it as a literal
* path rather than a glob pattern. This is necessary for paths containing characters like `(` and
* `)` (e.g. Next.js route group directories such as `src/(group)/...`) which would otherwise be
* interpreted as extglob patterns by fast-glob.
*/
export function escapeGlobPath(filePath: string): string {
return filePath.replace(/[()[\]{}!*?|+@]/g, '\\$&');
}

/** A Vite plugin that configures dependency optimization for Storybook's dev server. */
export function storybookOptimizeDepsPlugin(options: Options): Plugin {
return {
Expand Down Expand Up @@ -40,12 +50,14 @@ export function storybookOptimizeDepsPlugin(options: Options): Plugin {
// Story files + preview annotation files as entry points for the dep optimizer.
// Vite will crawl these to discover all transitive CJS dependencies that need
// pre-bundling, removing the need for a hard-coded include list.
// Paths are escaped so that special glob characters (e.g. parentheses in Next.js route
// group directories) are treated as literal characters, not glob syntax.
entries: [
...(typeof config.optimizeDeps?.entries === 'string'
? [config.optimizeDeps.entries]
: (config.optimizeDeps?.entries ?? [])),
...getUniqueImportPaths(index),
...previewAnnotationEntries,
...getUniqueImportPaths(index).map(escapeGlobPath),
...previewAnnotationEntries.map(escapeGlobPath),
],
// Extra deps explicitly included by Storybook presets (e.g. framework-specific packages).
include: [...extraOptimizeDeps, ...(config.optimizeDeps?.include ?? [])],
Expand Down
Loading