Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
be5ae52
initial index-based importFn generation
JReinhold Jan 28, 2025
21926a5
refactor vite hmr and importFn generation
JReinhold Feb 19, 2025
cc0b7fa
Merge branch 'next' of github.com:storybookjs/storybook into jeppe/cu…
JReinhold Feb 19, 2025
9dd972a
fix hmr
JReinhold Feb 20, 2025
0ffeef0
use StoryIndexGenerator when building too
JReinhold Feb 20, 2025
f6ba58a
fix importPath for virtual modules
JReinhold Feb 21, 2025
bd43701
cleanup
JReinhold Feb 21, 2025
23996ff
Merge branch 'next' into jeppe/custom-indexer-importpath
JReinhold Feb 21, 2025
c7ce2ac
Merge branch 'next' of github.com:storybookjs/storybook into jeppe/cu…
JReinhold Sep 29, 2025
c0b65de
make storyIndexGenerator a preset property
JReinhold Sep 29, 2025
567fe81
make importPath optional
JReinhold Sep 29, 2025
beb26c1
rename storiesJson -> indexJson
JReinhold Sep 29, 2025
1e7bf42
improve importfn paths, tests
JReinhold Sep 29, 2025
c2528fe
fix invalid import paths
JReinhold Sep 30, 2025
43b968e
Merge branch 'next' into jeppe/custom-indexer-importpath
JReinhold Oct 1, 2025
13f9a62
cleanup
JReinhold Oct 1, 2025
28bb2f5
Merge branch 'jeppe/custom-indexer-importpath' of github.com:storyboo…
JReinhold Oct 1, 2025
8b9df0a
silent weird ts error
JReinhold Oct 1, 2025
137980d
fix checks
JReinhold Oct 2, 2025
416b534
Merge branch 'next' of github.com:storybookjs/storybook into jeppe/cu…
JReinhold Oct 6, 2025
df56ebf
Merge branch 'next' of github.com:storybookjs/storybook into jeppe/cu…
JReinhold Dec 9, 2025
40e0d9d
make import entries unique in optimize deps
JReinhold Dec 9, 2025
6e6a491
Merge branch 'next' into jeppe/custom-indexer-importpath
JReinhold Dec 11, 2025
09d8aed
add vscode workspace
JReinhold Dec 11, 2025
9a0b8e5
Fix file change watching invalidating index
JReinhold Dec 11, 2025
de4b6de
Merge branch 'jeppe/custom-indexer-importpath' of github.com:storyboo…
JReinhold Dec 11, 2025
2f20882
Merge branch 'next' into jeppe/custom-indexer-importpath
JReinhold Dec 11, 2025
4c43d3d
fix build not passing normalizedStories to storyIndexGenerator preset
JReinhold Dec 11, 2025
09b8db1
Merge branch 'jeppe/custom-indexer-importpath' of github.com:storyboo…
JReinhold Dec 11, 2025
a075997
make StoryIndexGenerator do specifier lookup instead of watchStorySpe…
JReinhold Dec 12, 2025
ac49974
fix potential race condition with story index generator
JReinhold Dec 12, 2025
f72345e
fix SIG tests
JReinhold Dec 15, 2025
2063b51
fix unit tests to match new event batching behavior
JReinhold Dec 16, 2025
25e76c9
remove comment
JReinhold Dec 16, 2025
e5ece54
Merge branch 'next' into jeppe/custom-indexer-importpath
JReinhold Dec 16, 2025
920b476
cleanup
JReinhold Dec 16, 2025
c9c58c6
Merge branch 'jeppe/custom-indexer-importpath' of github.com:storyboo…
JReinhold Dec 16, 2025
d4cdc75
simplify index error handling
JReinhold Dec 16, 2025
c648147
support node 20 in map keys
JReinhold Dec 16, 2025
d4d5654
cleanup
JReinhold Dec 16, 2025
c3941b1
cleanup
JReinhold Dec 16, 2025
2f8ceda
improve variable name
JReinhold Dec 17, 2025
2121367
Merge branch 'next' into jeppe/custom-indexer-importpath
JReinhold Dec 18, 2025
43774a7
Merge branch 'next' into jeppe/custom-indexer-importpath
JReinhold Dec 18, 2025
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
5 changes: 0 additions & 5 deletions code/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,6 @@ module.exports = {
'error',
{
paths: [
{
name: 'vite',
message: 'Please dynamically import from vite instead, to force the use of ESM',
allowTypeImports: true,
},
{
name: 'react-aria',
message:
Expand Down
74 changes: 57 additions & 17 deletions code/builders/builder-vite/src/codegen-importfn-script.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
import { describe, expect, it, vi } from 'vitest';

import { toImportFn } from './codegen-importfn-script';
import type { StoryIndex } from 'storybook/internal/types';

describe('toImportFn', () => {
it('should correctly map story paths to import functions for absolute paths on Linux', async () => {
import { generateImportFnScriptCode } from './codegen-importfn-script';

describe('generateImportFnScriptCode', () => {
it('should correctly map story paths to import functions for POSIX paths', async () => {
vi.spyOn(process, 'cwd').mockReturnValue('/absolute/path');

const stories = ['/absolute/path/to/story1.js', '/absolute/path/to/story2.js'];
const index: StoryIndex = {
v: 5,
entries: {
'path-to-story': {
id: 'path-to-story',
title: 'Path to Story',
name: 'Default',
importPath: './to/abs-story.js',
type: 'story',
subtype: 'story',
},
'virtual-story': {
id: 'virtual-story',
title: 'Virtual Story',
name: 'Default',
importPath: 'virtual:story.js',
type: 'story',
subtype: 'story',
},
},
};

const result = await toImportFn(stories);
const result = generateImportFnScriptCode(index);

expect(result).toMatchInlineSnapshot(`
"const importers = {
"./to/story1.js": () => import("/absolute/path/to/story1.js"),
"./to/story2.js": () => import("/absolute/path/to/story2.js")
"./to/abs-story.js": () => import("/absolute/path/to/abs-story.js"),
"virtual:story.js": () => import("virtual:story.js")
};

export async function importFn(path) {
Expand All @@ -22,16 +44,37 @@ describe('toImportFn', () => {
`);
});

it('should correctly map story paths to import functions for absolute paths on Windows', async () => {
it('should correctly map story paths to import functions for Windows paths', async () => {
vi.spyOn(process, 'cwd').mockReturnValue('C:\\absolute\\path');
const stories = ['C:\\absolute\\path\\to\\story1.js', 'C:\\absolute\\path\\to\\story2.js'];

const result = await toImportFn(stories);
const index: StoryIndex = {
v: 5,
entries: {
'abs-path-to-story': {
id: 'abs-path-to-story',
title: 'Absolute Path to Story',
name: 'Default',
importPath: 'to\\abs-story.js',
type: 'story',
subtype: 'story',
},
'virtual-story': {
id: 'virtual-story',
title: 'Virtual Story',
name: 'Default',
importPath: 'virtual:story.js',
type: 'story',
subtype: 'story',
},
},
};

const result = generateImportFnScriptCode(index);

expect(result).toMatchInlineSnapshot(`
"const importers = {
"./to/story1.js": () => import("C:/absolute/path/to/story1.js"),
"./to/story2.js": () => import("C:/absolute/path/to/story2.js")
"./to/abs-story.js": () => import("C:/absolute/path/to/abs-story.js"),
"virtual:story.js": () => import("virtual:story.js")
};

export async function importFn(path) {
Expand All @@ -40,11 +83,8 @@ describe('toImportFn', () => {
`);
});

it('should handle an empty array of stories', async () => {
const root = '/absolute/path';
const stories: string[] = [];

const result = await toImportFn(stories);
it('should handle an empty index', async () => {
const result = generateImportFnScriptCode({ v: 5, entries: {} });

expect(result).toMatchInlineSnapshot(`
"const importers = {};
Expand Down
69 changes: 30 additions & 39 deletions code/builders/builder-vite/src/codegen-importfn-script.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
import type { Options } from 'storybook/internal/types';
import type { StoryIndex } from 'storybook/internal/types';

import { genDynamicImport, genObjectFromRawEntries } from 'knitwork';
import { normalize, relative } from 'pathe';
import { join, normalize, relative } from 'pathe';
import { dedent } from 'ts-dedent';

import { listStories } from './list-stories';
import { getUniqueImportPaths } from './utils/unique-import-paths';

/**
* This file is largely based on
* https://github.com/storybookjs/storybook/blob/d1195cbd0c61687f1720fefdb772e2f490a46584/lib/core-common/src/utils/to-importFn.ts
* This function takes the story index and creates a mapping between the stories' relative paths to
* the working directory and their dynamic imports. The import is done in an asynchronous function
* to delay loading and to allow Vite to split the code into smaller chunks. It then creates a
* function, `importFn(path)`, which resolves a path to an import function and this is called by
* Storybook to fetch a story dynamically when needed.
*/

/**
* Paths get passed either with no leading './' - e.g. `src/Foo.stories.js`, or with a leading `../`
* (etc), e.g. `../src/Foo.stories.js`. We want to deal in importPaths relative to the working dir,
* so we normalize
*/
function toImportPath(relativePath: string) {
return relativePath.startsWith('../') ? relativePath : `./${relativePath}`;
}

/**
* This function takes an array of stories and creates a mapping between the stories' relative paths
* to the working directory and their dynamic imports. The import is done in an asynchronous
* function to delay loading and to allow Vite to split the code into smaller chunks. It then
* creates a function, `importFn(path)`, which resolves a path to an import function and this is
* called by Storybook to fetch a story dynamically when needed.
*
* @param stories An array of absolute story paths.
*/
export async function toImportFn(stories: string[]) {
const objectEntries = stories.map((file) => {
const relativePath = relative(process.cwd(), file);

return [toImportPath(relativePath), genDynamicImport(normalize(file))] as [string, string];
});
export function generateImportFnScriptCode(index: StoryIndex): string {
const objectEntries: [path: string, importStatement: string][] = getUniqueImportPaths(index).map(
(importPath) => {
if (importPath.startsWith('virtual:')) {
return [importPath, genDynamicImport(importPath)];
}

/**
* Relative paths get passed either with no leading './' - e.g. 'src/Foo.stories.js', or with
* a leading '../', e.g. '../src/Foo.stories.js'. We want to deal in importPaths relative to
* the working dir, so we normalize
*/
const relativePath = normalize(relative(process.cwd(), importPath));
const normalizedRelativePath = relativePath.startsWith('../')
? relativePath
: `./${relativePath}`;

const absolutePath = normalize(join(process.cwd(), importPath));

return [normalizedRelativePath, genDynamicImport(absolutePath)];
}
);

return dedent`
const importers = ${genObjectFromRawEntries(objectEntries)};
Expand All @@ -44,12 +44,3 @@ export async function toImportFn(stories: string[]) {
}
`;
}

export async function generateImportFnScriptCode(options: Options): Promise<string> {
// First we need to get an array of stories and their absolute paths.
const stories = await listStories(options);

// We can then call toImportFn to create a function that can be used to load each story dynamically.

return await toImportFn(stories);
}
36 changes: 0 additions & 36 deletions code/builders/builder-vite/src/list-stories.ts

This file was deleted.

23 changes: 11 additions & 12 deletions code/builders/builder-vite/src/optimizeDeps.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { relative } from 'node:path';
import type { StoryIndexGenerator } from 'storybook/internal/core-server';
import type { Options, StoryIndex } from 'storybook/internal/types';

import type { Options } from 'storybook/internal/types';

import type { UserConfig, InlineConfig as ViteInlineConfig } from 'vite';
import { type UserConfig, type InlineConfig as ViteInlineConfig, resolveConfig } from 'vite';

import { INCLUDE_CANDIDATES } from './constants';
import { listStories } from './list-stories';
import { getUniqueImportPaths } from './utils/unique-import-paths';

/**
* Helper function which allows us to `filter` with an async predicate. Uses Promise.all for
Expand All @@ -17,12 +16,13 @@ const asyncFilter = async (arr: string[], predicate: (val: string) => Promise<bo
// TODO: This function should be reworked. The code it uses is outdated and we need to investigate
// More info: https://github.com/storybookjs/storybook/issues/32462#issuecomment-3421326557
export async function getOptimizeDeps(config: ViteInlineConfig, options: Options) {
const extraOptimizeDeps = await options.presets.apply('optimizeViteDeps', []);
const [extraOptimizeDeps, storyIndexGenerator] = await Promise.all([
options.presets.apply('optimizeViteDeps', []),
options.presets.apply<StoryIndexGenerator>('storyIndexGenerator'),
]);

const index: StoryIndex = await storyIndexGenerator.getIndex();

const { root = process.cwd() } = config;
const { normalizePath, resolveConfig } = await import('vite');
const absoluteStories = await listStories(options);
const stories = absoluteStories.map((storyPath) => normalizePath(relative(root, storyPath)));
// TODO: check if resolveConfig takes a lot of time, possible optimizations here
const resolvedConfig = await resolveConfig(config, 'serve', 'development');

Expand All @@ -33,8 +33,7 @@ export async function getOptimizeDeps(config: ViteInlineConfig, options: Options

const optimizeDeps: UserConfig['optimizeDeps'] = {
...config.optimizeDeps,
// We don't need to resolve the glob since vite supports globs for entries.
entries: stories,
entries: getUniqueImportPaths(index),
// We need Vite to precompile these dependencies, because they contain non-ESM code that would break
// if we served it directly to the browser.
include: [...include, ...extraOptimizeDeps, ...(config.optimizeDeps?.include || [])],
Expand Down
Loading