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
24 changes: 24 additions & 0 deletions code/core/src/manager-api/modules/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
API_ComposedRef,
API_ComposedRefUpdate,
API_IndexHash,
API_RefStoryRuntimeData,
API_Refs,
API_SetRefData,
API_StoryMapper,
Expand Down Expand Up @@ -103,6 +104,27 @@ const addRefIds = (input: API_IndexHash, ref: API_ComposedRef): API_IndexHash =>
}, {} as API_IndexHash);
};

// A composed ref's story/docs entries are enriched at runtime from the preview's STORY_PREPARED /
// DOCS_PREPARED events (args, argTypes, parameters, prepared) — data that is NOT in the ref's static
// story index. That enrichment is cached per ref (`ref.storyUpdates`) and re-applied here whenever
// `setRef` rebuilds the hash from the static index, so it is not wiped by re-composition and is
// applied even when it arrives before the index is first composed (the deep-link race in #34553).
const applyRefStoryUpdates = (
index: API_IndexHash,
storyUpdates: API_RefStoryRuntimeData | undefined
): API_IndexHash => {
if (!storyUpdates) {
return index;
}
Object.entries(storyUpdates).forEach(([id, update]) => {
const entry = index[id];
if (entry && (entry.type === 'story' || entry.type === 'docs')) {
index[id] = { ...entry, ...update } as API_IndexHash[string];
}
});
return index;
};

async function handleRequest(
request: Response | Promise<Response | boolean> | boolean
): Promise<API_SetRefData> {
Expand Down Expand Up @@ -339,9 +361,11 @@ export const init: ModuleFn<SubAPI, SubState> = (

if (index) {
index = addRefIds(index, ref);
index = applyRefStoryUpdates(index, ref?.storyUpdates);
}
if (filteredIndex) {
filteredIndex = addRefIds(filteredIndex, ref);
filteredIndex = applyRefStoryUpdates(filteredIndex, ref?.storyUpdates);
}
await api.updateRef(id, { ...ref, ...rest, index, filteredIndex, internal_index });
},
Expand Down
35 changes: 25 additions & 10 deletions code/core/src/manager-api/modules/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,13 @@ export const init: ModuleFn<SubAPI, SubState> = ({
}
} else {
const { id: refId, index, filteredIndex }: any = ref;
// Cache the runtime enrichment on the ref so it survives index (re)builds in `setRef` and is
// applied even when it arrives before the ref index is first composed (the deep-link race in
// #34553, where the preview sends STORY_PREPARED once, before the index has been composed).
const storyUpdates = {
...ref.storyUpdates,
[storyId]: { ...ref.storyUpdates?.[storyId], ...update },
};
Comment thread
ndelangen marked this conversation as resolved.
if (index && index[storyId]) {
index[storyId] = {
...index[storyId],
Expand All @@ -844,7 +851,7 @@ export const init: ModuleFn<SubAPI, SubState> = ({
...update,
} as API_StoryEntry;
}
await fullAPI.updateRef(refId, { index, filteredIndex });
await fullAPI.updateRef(refId, { index, filteredIndex, storyUpdates });
}
},
updateDocs: async (
Expand All @@ -871,15 +878,23 @@ export const init: ModuleFn<SubAPI, SubState> = ({
}
} else {
const { id: refId, index, filteredIndex }: any = ref;
index[docsId] = {
...index[docsId],
...update,
} as API_DocsEntry;
filteredIndex[docsId] = {
...filteredIndex[docsId],
...update,
} as API_DocsEntry;
await fullAPI.updateRef(refId, { index, filteredIndex });
const storyUpdates = {
...ref.storyUpdates,
[docsId]: { ...ref.storyUpdates?.[docsId], ...update },
};
Comment thread
ndelangen marked this conversation as resolved.
if (index && index[docsId]) {
index[docsId] = {
...index[docsId],
...update,
} as API_DocsEntry;
}
if (filteredIndex && filteredIndex[docsId]) {
filteredIndex[docsId] = {
...filteredIndex[docsId],
...update,
} as API_DocsEntry;
}
await fullAPI.updateRef(refId, { index, filteredIndex, storyUpdates });
}
},
setPreviewInitialized: async (ref) => {
Expand Down
42 changes: 42 additions & 0 deletions code/core/src/manager-api/tests/refs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1331,6 +1331,48 @@ describe('Refs API', () => {
expect.objectContaining({ 'a--2': expect.anything() })
);
});

it('re-applies cached runtime story enrichment when rebuilding the index (#34553)', async () => {
const index: StoryIndex = {
v: 5,
entries: {
'a--1': {
id: 'a--1',
title: 'A',
name: '1',
importPath: './path/to/a1.ts',
type: 'story',
subtype: 'story',
},
},
};

// Simulate a ref whose story got enriched at runtime (via STORY_PREPARED) before/independent of
// its static index being (re)composed. The enrichment is cached on `storyUpdates`.
const initialState: Partial<State> = {
filters: {},
refs: {
fake: {
id: 'fake',
url: 'https://example.com',
previewInitialized: true,
storyUpdates: {
'a--1': { prepared: true, argTypes: { label: { name: 'label' } } as any },
},
},
},
};

const store = createMockStore(initialState);
const { api } = initRefs({ provider, store } as any, { runCheck: false });

// The incoming static index has no argTypes; the cached enrichment must survive the rebuild.
await api.setRef('fake', { storyIndex: index });

const rebuilt = api.getRefs().fake.index?.['a--1'] as any;
expect(rebuilt.argTypes).toEqual({ label: { name: 'label' } });
expect(rebuilt.prepared).toBe(true);
});
});

it('errors on unknown version', async () => {
Expand Down
2 changes: 2 additions & 0 deletions code/core/src/manager-api/tests/stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,8 @@ describe('stories API', () => {
expect(fullAPI.updateRef).toHaveBeenCalledWith('refId', {
filteredIndex: { 'a--1': { args: { foo: 'bar' } } },
index: { 'a--1': { args: { foo: 'bar' } } },
// Runtime enrichment is also cached on the ref so it survives index rebuilds (#34553).
storyUpdates: { 'a--1': { args: { foo: 'bar' } } },
});
});
it('updateStoryArgs emits UPDATE_STORY_ARGS to the local frame and does not change anything', () => {
Expand Down
10 changes: 10 additions & 0 deletions code/core/src/types/modules/api-stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ export interface API_TestEntry extends Omit<API_StoryEntry, 'subtype' | 'childre
}

export type API_LeafEntry = API_DocsEntry | API_StoryEntry | API_TestEntry;

/**
* Runtime enrichment for a composed ref's stories/docs, keyed by entry id. These fields come from
* the ref preview's STORY_PREPARED / DOCS_PREPARED events and are not part of the ref's static story
* index, so they are cached per ref and re-applied whenever the ref index is (re)built.
*/
Comment thread
ndelangen marked this conversation as resolved.
export type API_RefStoryRuntimeData = Record<
StoryId,
Partial<Pick<API_StoryEntry, 'prepared' | 'parameters' | 'args' | 'argTypes' | 'initialArgs'>>
>;
export type API_HashEntry =
| API_RootEntry
| API_GroupEntry
Expand Down
8 changes: 8 additions & 0 deletions code/core/src/types/modules/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
API_HashEntry,
API_IndexHash,
API_PreparedIndexEntry,
API_RefStoryRuntimeData,
} from './api-stories.ts';
import type { SetStoriesStory, SetStoriesStoryData } from './channelApi.ts';
import type { DocsOptions } from './core-common.ts';
Expand Down Expand Up @@ -150,6 +151,12 @@ export interface API_LoadedRefData {
filteredIndex?: API_IndexHash;
indexError?: Error;
previewInitialized: boolean;
/**
* Runtime story enrichment (args, argTypes, parameters, initialArgs, prepared) received from the
* ref preview via STORY_PREPARED / DOCS_PREPARED, cached so it survives ref index (re)builds. See
* `API_RefStoryRuntimeData`.
*/
storyUpdates?: API_RefStoryRuntimeData;
}

export interface API_ComposedRef extends API_LoadedRefData {
Expand Down Expand Up @@ -181,6 +188,7 @@ export type API_ComposedRefUpdate = Partial<
| 'previewInitialized'
| 'sourceUrl'
| 'internal_index'
| 'storyUpdates'
>
>;

Expand Down