Skip to content
Closed
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 @@ -100,6 +100,47 @@ describe('referenceMeta', () => {
expect(context.storyById()).toEqual(story);
});

it('works with different module namespace objects when there is no default 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 firstCsfParts = csfFileParts('first-meta--first-story', 'first-meta', {
includeDefaultExport: false,
});
const secondCsfParts = csfFileParts('second-meta--second-story', 'second-meta', {
includeDefaultExport: false,
});
const store = {
componentStoriesFromCSFFile: ({ csfFile }: { csfFile: CSFFile }) =>
csfFile === firstCsfParts.csfFile ? [firstCsfParts.story] : [secondCsfParts.story],
} as unknown as StoryStore<Renderer>;
const context = new DocsContext(channel, store, renderStoryToElement, [
firstCsfParts.csfFile,
secondCsfParts.csfFile,
]);

const mixedModuleExports = {
first: firstCsfParts.storyExport,
second: secondCsfParts.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?'
);
});

it('throws for non-module objects (components)', () => {
const { story, csfFile, component } = csfFileParts();
const store = {
Expand Down Expand Up @@ -149,14 +190,43 @@ describe('resolveOf', () => {

it('works for module exports with different object identity but same default (Rolldown compatibility)', () => {
// Simulate what happens in Rolldown: different namespace object but same default export
const differentModuleExports = { default: metaExport, story: storyExport };
const differentModuleExports = {
default: metaExport,
story: storyExport,
};
expect(context.resolveOf(differentModuleExports)).toEqual({
type: 'meta',
csfFile,
preparedMeta: expect.any(Object),
});
});

it('works for module exports with different object identity and only shared story exports', () => {
const noDefaultCsfParts = csfFileParts(
'meta--story-without-default',
'meta-without-default',
{
includeDefaultExport: false,
}
);
const noDefaultStore = {
componentStoriesFromCSFFile: () => [noDefaultCsfParts.story],
preparedMetaFromCSFFile: () => ({ prepareMeta: 'preparedMeta' }),
projectAnnotations,
} as unknown as StoryStore<Renderer>;
const noDefaultContext = new DocsContext(channel, noDefaultStore, renderStoryToElement, [
noDefaultCsfParts.csfFile,
]);

noDefaultContext.attachCSFFile(noDefaultCsfParts.csfFile);

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

it('works for components', () => {
expect(context.resolveOf(component)).toEqual({
type: 'component',
Expand Down Expand Up @@ -187,7 +257,10 @@ describe('resolveOf', () => {

describe('validation allowed', () => {
it('works for story exports', () => {
expect(context.resolveOf(storyExport, ['story'])).toEqual({ type: 'story', story });
expect(context.resolveOf(storyExport, ['story'])).toEqual({
type: 'story',
story,
});
});

it('works for meta exports', () => {
Expand Down Expand Up @@ -215,7 +288,10 @@ describe('resolveOf', () => {
});

it('finds primary story', () => {
expect(context.resolveOf('story', ['story'])).toEqual({ type: 'story', story });
expect(context.resolveOf('story', ['story'])).toEqual({
type: 'story',
story,
});
});

it('finds attached CSF file', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
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);
if ('default' in csfFile.moduleExports) {
this.exportsToCSFFile.set(csfFile.moduleExports.default, csfFile);
}

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

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

if (!csfFile && moduleExportOrType && typeof moduleExportOrType === 'object') {
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;
continue;
}

if (matchedCSFFile !== storyCSFFile) {
matchedCSFFile = undefined;
break;
}
}

csfFile = matchedCSFFile;
}

if (csfFile) {
return { type: 'meta', csfFile } as TResolvedExport;
}
Expand All @@ -184,7 +218,10 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
}

// If the export isn't a module, default or story export, we assume it is a component
return { type: 'component', component: moduleExportOrType } as TResolvedExport;
return {
type: 'component',
component: moduleExportOrType,
} as TResolvedExport;
}

resolveOf<TType extends ResolvedModuleExportType>(
Expand Down Expand Up @@ -221,7 +258,9 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
case 'meta': {
return {
...resolved,
preparedMeta: this.store.preparedMetaFromCSFFile({ csfFile: resolved.csfFile }),
preparedMeta: this.store.preparedMetaFromCSFFile({
csfFile: resolved.csfFile,
}),
};
}
case 'story':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
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 = {
id: storyId,
moduleExport: storyExport,
} as CSFFile['stories'][string];
const story = { id: storyId, moduleExport: storyExport } as PreparedStory;
const meta = { id: metaId, title: 'Meta', component, moduleExports } as CSFFile['meta'];
const meta = {
id: metaId,
title: 'Meta',
component,
moduleExports,
} as CSFFile['meta'];
const csfFile = {
stories: { [storyId]: storyAnnotations },
meta,
Expand Down
Loading