Skip to content

Commit 9efd8af

Browse files
authored
Merge pull request storybookjs#35323 from storybookjs/fix/preview-core-annotations
Core: Compose core annotations before preview beforeAll
2 parents 4cac956 + ff2a9a8 commit 9efd8af

6 files changed

Lines changed: 74 additions & 41 deletions

File tree

code/core/src/preview-api/modules/preview-web/Preview.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import type {
4545

4646
import { global } from '@storybook/global';
4747

48-
import { StoryStore } from '../../store.ts';
48+
import { StoryStore, composeProjectAnnotationsWithCore } from '../../store.ts';
4949
import { addons } from '../addons/index.ts';
5050
import type { CsfDocsRender } from './render/CsfDocsRender.ts';
5151
import type { MdxDocsRender } from './render/MdxDocsRender.ts';
@@ -154,7 +154,14 @@ export class Preview<TRenderer extends Renderer> {
154154

155155
async getProjectAnnotationsOrRenderError(): Promise<ProjectAnnotations<TRenderer>> {
156156
try {
157-
const projectAnnotations = await this.getProjectAnnotations();
157+
// Compose the core annotations in here (the same way the store and portable stories do) so the
158+
// `beforeAll` hooks they contribute — e.g. registering the `core/docgen` and `core/story-docs`
159+
// services in the preview — run at init. Plain (CSF1-3) previews don't fold core annotations
160+
// into `getProjectAnnotations()`, and core annotations would otherwise only be prepended later,
161+
// inside the store, which is too late for the `beforeAll` we run below.
162+
const projectAnnotations = composeProjectAnnotationsWithCore<TRenderer>([
163+
await this.getProjectAnnotations(),
164+
]);
158165

159166
this.renderToCanvas = projectAnnotations.renderToCanvas;
160167

code/core/src/preview-api/modules/preview-web/PreviewWeb.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3627,9 +3627,11 @@ describe('PreviewWeb', () => {
36273627
await waitForRender();
36283628

36293629
// @ts-expect-error Ignore protected property
3630-
expect((preview.storyStoreValue as StoryStore<Renderer>)!.userGlobals.get()).toEqual({
3631-
a: 'edited',
3632-
});
3630+
expect((preview.storyStoreValue as StoryStore<Renderer>)!.userGlobals.get()).toEqual(
3631+
expect.objectContaining({
3632+
a: 'edited',
3633+
})
3634+
);
36333635
});
36343636

36353637
it('emits SET_GLOBALS with new values', async () => {
@@ -3642,7 +3644,7 @@ describe('PreviewWeb', () => {
36423644

36433645
await waitForEvents([SET_GLOBALS]);
36443646
expect(mockChannel.emit).toHaveBeenCalledWith(SET_GLOBALS, {
3645-
globals: { a: 'edited' },
3647+
globals: expect.objectContaining({ a: 'edited' }),
36463648
globalTypes: {},
36473649
});
36483650
});
@@ -3707,7 +3709,7 @@ describe('PreviewWeb', () => {
37073709
expect.objectContaining({
37083710
storyContext: expect.objectContaining({
37093711
args: { foo: 'a', one: 'mapped-1', global: 'added' },
3710-
globals: { a: 'edited' },
3712+
globals: expect.objectContaining({ a: 'edited' }),
37113713
}),
37123714
}),
37133715
'story-element'

code/core/src/preview-api/modules/store/StoryStore.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { CleanupCallback } from 'storybook/internal/csf';
2-
import { getCoreAnnotations, hasCoreAnnotations } from 'storybook/internal/csf';
32
import {
43
CalledExtractOnStoreError,
54
MissingStoryFromCsfFileError,
@@ -28,7 +27,7 @@ import { ArgsStore } from './ArgsStore.ts';
2827
import { GlobalsStore } from './GlobalsStore.ts';
2928
import { StoryIndexStore } from './StoryIndexStore.ts';
3029
import {
31-
composeConfigs,
30+
composeProjectAnnotationsWithCore,
3231
normalizeProjectAnnotations,
3332
prepareContext,
3433
prepareMeta,
@@ -45,20 +44,13 @@ export function picky<T extends Record<string, any>, K extends keyof T>(
4544
}
4645

4746
/**
48-
* Compose and normalize the project annotations for the store, prepending the core annotations.
49-
*
50-
* CSF4 previews (created with `definePreview`) already compose the core annotations into their
51-
* `composed` result and flag it via {@link hasCoreAnnotations}. In that case we must NOT prepend
52-
* core annotations again, otherwise every core decorator/loader/beforeEach/beforeAll would run
53-
* twice for non-factory (CSF1/2/3) stories that fall back to `store.projectAnnotations`.
47+
* Compose and normalize the project annotations for the store, prepending the core annotations via
48+
* {@link composeProjectAnnotationsWithCore} (CSF4 previews are detected and not double-composed).
5449
*/
5550
function composeProjectAnnotations<TRenderer extends Renderer>(
5651
projectAnnotations: ProjectAnnotations<TRenderer>
5752
): NormalizedProjectAnnotations<TRenderer> {
58-
if (hasCoreAnnotations(projectAnnotations)) {
59-
return normalizeProjectAnnotations(projectAnnotations);
60-
}
61-
return normalizeProjectAnnotations(composeConfigs([...getCoreAnnotations(), projectAnnotations]));
53+
return normalizeProjectAnnotations(composeProjectAnnotationsWithCore([projectAnnotations]));
6254
}
6355

6456
// TODO -- what are reasonable values for these?
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {
2+
getCoreAnnotations,
3+
hasCoreAnnotations,
4+
markAsComposedWithCoreAnnotations,
5+
} from 'storybook/internal/csf';
6+
import type {
7+
ModuleExports,
8+
NormalizedProjectAnnotations,
9+
Renderer,
10+
} from 'storybook/internal/types';
11+
12+
import { composeConfigs } from './composeConfigs.ts';
13+
14+
/**
15+
* Composes project annotations together, prepending Storybook's core annotations unless they are
16+
* already present, and flags the result via {@link markAsComposedWithCoreAnnotations} so later
17+
* composition does not add them a second time.
18+
*
19+
* Core annotations carry behavior that must be applied exactly once per realm: decorators, loaders,
20+
* `beforeEach`, and the `beforeAll` hooks that register the built-in open services (`core/docgen`,
21+
* `core/story-docs`). CSF4 previews (`definePreview`) already fold the core annotations into their
22+
* `composed` result and mark it; plain CSF1-3 previews do not. Every entrypoint that runs `beforeAll`
23+
* or builds the story store off the raw project annotations (the browser preview, the store, and
24+
* portable stories) funnels them through here so the behavior is identical across all of them.
25+
*
26+
* Pass the *raw* annotation modules (not pre-composed) so the CSF4 marker survives the
27+
* `hasCoreAnnotations` check. The result is composed but NOT normalized — callers that need a
28+
* {@link NormalizedProjectAnnotations} should run `normalizeProjectAnnotations` on top.
29+
*/
30+
export function composeProjectAnnotationsWithCore<TRenderer extends Renderer = Renderer>(
31+
annotations: ModuleExports[]
32+
): NormalizedProjectAnnotations<TRenderer> {
33+
// Drop falsy entries (e.g. `setProjectAnnotations(undefined)` or a sparse array): `composeConfigs`
34+
// reads `.default` off each entry and would throw on `null`/`undefined`.
35+
const definedAnnotations = annotations.filter(Boolean);
36+
const alreadyComposedWithCore = definedAnnotations.some((annotation) =>
37+
hasCoreAnnotations(annotation)
38+
);
39+
return markAsComposedWithCoreAnnotations(
40+
composeConfigs<TRenderer>(
41+
alreadyComposedWithCore
42+
? definedAnnotations
43+
: [...getCoreAnnotations(), ...definedAnnotations]
44+
)
45+
);
46+
}

code/core/src/preview-api/modules/store/csf/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './normalizeProjectAnnotations.ts';
77
export * from './normalizeArrays.ts';
88
export * from './getValuesFromGlobalTypes.ts';
99
export * from './composeConfigs.ts';
10+
export * from './composeProjectAnnotationsWithCore.ts';
1011
export * from './stepRunners.ts';
1112
export * from './portable-stories.ts';
1213
export * from './csf-factory-utils.ts';

code/core/src/preview-api/modules/store/csf/portable-stories.ts

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { type CleanupCallback, isExportStory } from 'storybook/internal/csf';
2-
import { getCoreAnnotations, hasCoreAnnotations } from 'storybook/internal/csf';
32
import { MountMustBeDestructuredError } from 'storybook/internal/preview-errors';
43
import type {
54
Args,
@@ -31,6 +30,7 @@ import {
3130
} from '../../preview-web/render/animation-utils.ts';
3231
import { ReporterAPI } from '../reporter-api.ts';
3332
import { composeConfigs } from './composeConfigs.ts';
33+
import { composeProjectAnnotationsWithCore } from './composeProjectAnnotationsWithCore.ts';
3434
import { getCsfFactoryAnnotations } from './csf-factory-utils.ts';
3535
import { getValuesFromGlobalTypes } from './getValuesFromGlobalTypes.ts';
3636
import { normalizeComponentAnnotations } from './normalizeComponentAnnotations.ts';
@@ -56,34 +56,19 @@ export function setDefaultProjectAnnotations<TRenderer extends Renderer = Render
5656
const DEFAULT_STORY_TITLE = 'ComposedStory';
5757
const DEFAULT_STORY_NAME = 'Unnamed Story';
5858

59-
function extractAnnotation<TRenderer extends Renderer = Renderer>(
60-
annotation: NamedOrDefaultProjectAnnotations<TRenderer>
61-
) {
62-
if (!annotation) {
63-
return {};
64-
}
65-
// support imports such as
66-
// import * as annotations from '.storybook/preview'
67-
// import annotations from '.storybook/preview'
68-
// in both cases: 1 - the file has a default export; 2 - named exports only
69-
// also support when the file has both annotations coming from default and named exports
70-
return composeConfigs([annotation]);
71-
}
72-
7359
export function setProjectAnnotations<TRenderer extends Renderer = Renderer>(
7460
projectAnnotations:
7561
| NamedOrDefaultProjectAnnotations<TRenderer>
7662
| NamedOrDefaultProjectAnnotations<TRenderer>[]
7763
): NormalizedProjectAnnotations<TRenderer> {
7864
const annotations = Array.isArray(projectAnnotations) ? projectAnnotations : [projectAnnotations];
79-
// CSF4 previews (definePreview) already compose the core annotations into their `composed`
80-
// result, which is what e.g. addon-vitest passes here via `getProjectAnnotations()`. Detect that
81-
// marker and skip prepending core annotations so they are not applied twice.
82-
const alreadyComposedWithCore = annotations.some((annotation) => hasCoreAnnotations(annotation));
83-
globalThis.globalProjectAnnotations = composeConfigs([
84-
...(alreadyComposedWithCore ? [] : getCoreAnnotations()),
65+
// Pass the raw annotation modules (which may use `default` and/or named exports, e.g. from
66+
// `import * as annotations from '.storybook/preview'`) straight through: `composeConfigs` unwraps
67+
// those, and `composeProjectAnnotationsWithCore` detects the CSF4 marker (set by `definePreview`,
68+
// which is what e.g. addon-vitest passes here) so core annotations are never applied twice.
69+
globalThis.globalProjectAnnotations = composeProjectAnnotationsWithCore<TRenderer>([
8570
globalThis.defaultProjectAnnotations ?? {},
86-
composeConfigs(annotations.map(extractAnnotation)),
71+
...annotations,
8772
]);
8873

8974
return globalThis.globalProjectAnnotations ?? {};

0 commit comments

Comments
 (0)