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
Expand Up @@ -112,6 +112,50 @@ describe('referenceMeta', () => {
'<Meta of={} /> must reference a CSF file module export or meta export. Did you mistakenly reference your component instead of your CSF file?'
);
});

it('works with different module namespace objects when there is no default export', () => {
// Simulates CSF4 modules (no `export default meta`) split into a chunk:
// the MDX-imported namespace differs by identity from the one Storybook registered.
// Resolution should fall back to looking up the CSF file via any story export.
const { story, csfFile, storyExport } = csfFileParts('meta--story', 'meta', {
includeDefaultExport: false,
});
const store = {
componentStoriesFromCSFFile: () => [story],
} as unknown as StoryStore<Renderer>;
const context = new DocsContext(channel, store, renderStoryToElement, [csfFile]);

const differentModuleExports = { story: storyExport };

expect(() => context.referenceMeta(differentModuleExports, true)).not.toThrow();
expect(context.storyById()).toEqual(story);
});

it('throws for module objects whose story exports span multiple CSF files', () => {
const firstParts = csfFileParts('first-meta--first-story', 'first-meta', {
includeDefaultExport: false,
});
const secondParts = csfFileParts('second-meta--second-story', 'second-meta', {
includeDefaultExport: false,
});
const store = {
componentStoriesFromCSFFile: ({ csfFile }: { csfFile: CSFFile }) =>
csfFile === firstParts.csfFile ? [firstParts.story] : [secondParts.story],
} as unknown as StoryStore<Renderer>;
const context = new DocsContext(channel, store, renderStoryToElement, [
firstParts.csfFile,
secondParts.csfFile,
]);

const mixedModuleExports = {
first: firstParts.storyExport,
second: secondParts.storyExport,
};

expect(() => context.referenceMeta(mixedModuleExports, true)).toThrow(
'<Meta of={} /> must reference a CSF file module export or meta export. Did you mistakenly reference your component instead of your CSF file?'
);
});
});

describe('resolveOf', () => {
Expand Down Expand Up @@ -157,6 +201,38 @@ describe('resolveOf', () => {
});
});

it('works for CSF4 module exports with different object identity and no default export', () => {
// CSF4 modules (no `export default meta`) may be split into a separate chunk,
// producing a namespace object whose identity differs from Storybook's record.
// Resolution falls back to identifying the CSF file via the story exports.
const noDefaultParts = csfFileParts('meta--no-default-story', 'meta-no-default', {
includeDefaultExport: false,
});
const noDefaultStore = {
componentStoriesFromCSFFile: () => [noDefaultParts.story],
preparedMetaFromCSFFile: () => ({ prepareMeta: 'preparedMeta' }),
projectAnnotations,
} as unknown as StoryStore<Renderer>;
const noDefaultContext = new DocsContext(channel, noDefaultStore, renderStoryToElement, [
noDefaultParts.csfFile,
]);
noDefaultContext.attachCSFFile(noDefaultParts.csfFile);

expect(noDefaultContext.resolveOf({ story: noDefaultParts.storyExport })).toEqual({
type: 'meta',
csfFile: noDefaultParts.csfFile,
preparedMeta: expect.any(Object),
});
});

it('resolves a CSF4 Story object to its story, not its containing CSF file', () => {
// A CSF4 Story (detected via `_tag === 'Story'`) is an individual export,
// not a namespace. The namespace fallback must skip it so that
// <Canvas of={Stories.Primary} /> still resolves to the Primary story.
const csf4Story = { _tag: 'Story', input: storyExport };
expect(context.resolveOf(csf4Story, ['story'])).toEqual({ type: 'story', story });
});

it('works for components', () => {
expect(context.resolveOf(component)).toEqual({
type: 'component',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,11 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
referenceCSFFile(csfFile: CSFFile<TRenderer>) {
this.exportsToCSFFile.set(csfFile.moduleExports, csfFile);
// Also set the default export as the component's exports,
// to allow `import ButtonStories from './Button.stories'`
this.exportsToCSFFile.set(csfFile.moduleExports.default, csfFile);
// to allow `import ButtonStories from './Button.stories'`.
// CSF4 modules may not have a default export, so guard against it.
if ('default' in csfFile.moduleExports) {
this.exportsToCSFFile.set(csfFile.moduleExports.default, csfFile);
}

const stories = this.store.componentStoriesFromCSFFile({ csfFile });

Expand Down Expand Up @@ -171,6 +174,40 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
csfFile = this.exportsToCSFFile.get((moduleExportOrType as ModuleExports).default);
}

// CSF4 modules don't have a default export, and when a bundler splits the
// story module into a separate chunk the namespace passed to <Meta of={...} />
// may differ by object identity from the one Storybook registered. Fall back
// to resolving the CSF file via any of its story exports.
// Skip individual story objects (handled by the story lookup below).
if (
!csfFile &&
moduleExportOrType &&
typeof moduleExportOrType === 'object' &&
!isStory(moduleExportOrType)
) {
let matchedCSFFile: CSFFile<TRenderer> | undefined;
for (const exportValue of Object.values(moduleExportOrType as ModuleExports)) {
const story = this.exportToStory.get(
isStory(exportValue) ? exportValue.input : exportValue
);
if (!story) {
continue;
}
const storyCSFFile = this.storyIdToCSFFile.get(story.id);
if (!storyCSFFile) {
continue;
}
if (!matchedCSFFile) {
matchedCSFFile = storyCSFFile;
} else if (matchedCSFFile !== storyCSFFile) {
// Story exports span multiple CSF files — ambiguous, reject.
matchedCSFFile = undefined;
break;
}
}
csfFile = matchedCSFFile;
}

if (csfFile) {
return { type: 'meta', csfFile } as TResolvedExport;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import type { CSFFile, PreparedStory } from 'storybook/internal/types';

export function csfFileParts(storyId = 'meta--story', metaId = 'meta') {
export function csfFileParts(
storyId = 'meta--story',
metaId = 'meta',
{ includeDefaultExport = true }: { includeDefaultExport?: boolean } = {}
) {
// These compose the raw exports of the CSF file
const component = {};
const metaExport = { component };
const storyExport = {};
const moduleExports = { default: metaExport, story: storyExport };
const moduleExports = includeDefaultExport
? { default: metaExport, story: storyExport }
: { story: storyExport };

// This is the prepared story + CSF file after SB has processed them
const storyAnnotations = {
Expand Down
Loading