mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 07:21:17 +08:00
refactor prepareMeta and prepareStory
This commit is contained in:
parent
3063e85ed7
commit
f7f46f067d
@ -728,20 +728,21 @@ describe('prepareMeta', () => {
|
||||
},
|
||||
};
|
||||
const preparedStory = prepareStory({ id, name, moduleExport }, meta, { render });
|
||||
const preparedMeta = prepareMeta(meta, { render });
|
||||
const preparedMeta = prepareMeta(meta, { render }, {});
|
||||
|
||||
expect(preparedMeta).toMatchObject({
|
||||
...preparedStory,
|
||||
// properties that are actually different between prepareMeta and prepareStory
|
||||
moduleExport: undefined,
|
||||
id: 'id--__meta',
|
||||
name: '__meta',
|
||||
story: '__meta',
|
||||
// jest doesn't think two functions are equal, so we have assert that it's just any function
|
||||
applyLoaders: expect.any(Function),
|
||||
originalStoryFn: expect.any(Function),
|
||||
unboundStoryFn: expect.any(Function),
|
||||
undecoratedStoryFn: expect.any(Function),
|
||||
});
|
||||
// omitting the properties from preparedStory that are not in preparedMeta
|
||||
const {
|
||||
name: storyName,
|
||||
story,
|
||||
applyLoaders,
|
||||
originalStoryFn,
|
||||
unboundStoryFn,
|
||||
undecoratedStoryFn,
|
||||
playFunction,
|
||||
...expectedPreparedMeta
|
||||
} = preparedStory;
|
||||
|
||||
expect(preparedMeta).toMatchObject(expectedPreparedMeta);
|
||||
expect(Object.keys(preparedMeta)).toHaveLength(Object.keys(expectedPreparedMeta).length);
|
||||
});
|
||||
});
|
||||
|
@ -19,6 +19,8 @@ import type {
|
||||
StoryContextForEnhancers,
|
||||
StoryContextForLoaders,
|
||||
StrictArgTypes,
|
||||
PreparedMeta,
|
||||
ModuleExport,
|
||||
} from '@storybook/types';
|
||||
import { includeConditionalArg } from '@storybook/csf';
|
||||
|
||||
@ -49,56 +51,53 @@ export function prepareStory<TRenderer extends Renderer>(
|
||||
// NOTE: in the current implementation we are doing everything once, up front, rather than doing
|
||||
// anything at render time. The assumption is that as we don't load all the stories at once, this
|
||||
// will have a limited cost. If this proves misguided, we can refactor it.
|
||||
|
||||
return prepareAnnotations(storyAnnotations, componentAnnotations, projectAnnotations);
|
||||
}
|
||||
|
||||
export function prepareMeta<TRenderer extends Renderer>(
|
||||
componentAnnotations: NormalizedComponentAnnotations<TRenderer>,
|
||||
projectAnnotations: NormalizedProjectAnnotations<TRenderer>
|
||||
): PreparedStory<TRenderer> {
|
||||
return prepareAnnotations(undefined, componentAnnotations, projectAnnotations);
|
||||
}
|
||||
|
||||
function prepareAnnotations<TRenderer extends Renderer>(
|
||||
storyAnnotations: NormalizedStoryAnnotations<TRenderer> | undefined,
|
||||
componentAnnotations: NormalizedComponentAnnotations<TRenderer>,
|
||||
projectAnnotations: NormalizedProjectAnnotations<TRenderer>
|
||||
): PreparedStory<TRenderer> {
|
||||
// NOTE: in the current implementation we are doing everything once, up front, rather than doing
|
||||
// anything at render time. The assumption is that as we don't load all the stories at once, this
|
||||
// will have a limited cost. If this proves misguided, we can refactor it.
|
||||
|
||||
const { moduleExport, id, name } = storyAnnotations || {};
|
||||
const { title } = componentAnnotations;
|
||||
|
||||
const tags = [...(storyAnnotations?.tags || componentAnnotations.tags || []), 'story'];
|
||||
|
||||
const parameters: Parameters = combineParameters(
|
||||
projectAnnotations.parameters,
|
||||
componentAnnotations.parameters,
|
||||
storyAnnotations?.parameters
|
||||
const partialAnnotations = preparePartialAnnotations(
|
||||
storyAnnotations,
|
||||
componentAnnotations,
|
||||
projectAnnotations
|
||||
);
|
||||
|
||||
const decorators = [
|
||||
...(storyAnnotations?.decorators || []),
|
||||
...(componentAnnotations.decorators || []),
|
||||
...(projectAnnotations.decorators || []),
|
||||
];
|
||||
|
||||
// Currently it is only possible to set these globally
|
||||
const {
|
||||
applyDecorators = defaultDecorateStory,
|
||||
argTypesEnhancers = [],
|
||||
argsEnhancers = [],
|
||||
runStep,
|
||||
} = projectAnnotations;
|
||||
|
||||
const loaders = [
|
||||
...(projectAnnotations.loaders || []),
|
||||
...(componentAnnotations.loaders || []),
|
||||
...(storyAnnotations?.loaders || []),
|
||||
];
|
||||
const applyLoaders = async (context: StoryContextForLoaders<TRenderer>) => {
|
||||
const loadResults = await Promise.all(loaders.map((loader) => loader(context)));
|
||||
const loaded = Object.assign({}, ...loadResults);
|
||||
return { ...context, loaded };
|
||||
};
|
||||
|
||||
const undecoratedStoryFn: LegacyStoryFn<TRenderer> = (context: StoryContext<TRenderer>) => {
|
||||
const mappedArgs = Object.entries(context.args).reduce((acc, [key, val]) => {
|
||||
const mapping = context.argTypes[key]?.mapping;
|
||||
acc[key] = mapping && val in mapping ? mapping[val] : val;
|
||||
return acc;
|
||||
}, {} as Args);
|
||||
|
||||
const includedArgs = Object.entries(mappedArgs).reduce((acc, [key, val]) => {
|
||||
const argType = context.argTypes[key] || {};
|
||||
if (includeConditionalArg(argType, mappedArgs, context.globals)) acc[key] = val;
|
||||
return acc;
|
||||
}, {} as Args);
|
||||
|
||||
const includedContext = { ...context, args: includedArgs };
|
||||
const { passArgsFirst: renderTimePassArgsFirst = true } = context.parameters;
|
||||
return renderTimePassArgsFirst
|
||||
? (render as ArgsStoryFn<TRenderer>)(includedContext.args, includedContext)
|
||||
: (render as LegacyStoryFn<TRenderer>)(includedContext);
|
||||
};
|
||||
|
||||
// Currently it is only possible to set these globally
|
||||
const { applyDecorators = defaultDecorateStory, runStep } = projectAnnotations;
|
||||
|
||||
const decorators = [
|
||||
...(storyAnnotations?.decorators || []),
|
||||
...(componentAnnotations.decorators || []),
|
||||
...(projectAnnotations.decorators || []),
|
||||
];
|
||||
|
||||
// The render function on annotations *has* to be an `ArgsStoryFn`, so when we normalize
|
||||
// CSFv1/2, we use a new field called `userStoryFn` so we know that it can be a LegacyStoryFn
|
||||
@ -107,8 +106,95 @@ function prepareAnnotations<TRenderer extends Renderer>(
|
||||
storyAnnotations?.render ||
|
||||
componentAnnotations.render ||
|
||||
projectAnnotations.render;
|
||||
|
||||
if (!render) throw new Error(`No render function available for storyId '${id}'`);
|
||||
|
||||
const decoratedStoryFn = applyHooks<TRenderer>(applyDecorators)(undecoratedStoryFn, decorators);
|
||||
const unboundStoryFn = (context: StoryContext<TRenderer>) => {
|
||||
let finalContext: StoryContext<TRenderer> = context;
|
||||
if (global.FEATURES?.argTypeTargetsV7) {
|
||||
const argsByTarget = groupArgsByTarget(context);
|
||||
finalContext = {
|
||||
...context,
|
||||
allArgs: context.args,
|
||||
argsByTarget,
|
||||
args: argsByTarget[NO_TARGET_NAME] || {},
|
||||
};
|
||||
}
|
||||
|
||||
return decoratedStoryFn(finalContext);
|
||||
};
|
||||
|
||||
const play = storyAnnotations?.play || componentAnnotations.play;
|
||||
|
||||
const playFunction =
|
||||
play &&
|
||||
(async (storyContext: StoryContext<TRenderer>) => {
|
||||
const playFunctionContext: PlayFunctionContext<TRenderer> = {
|
||||
...storyContext,
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
step: (label: StepLabel, play: PlayFunction<TRenderer>) =>
|
||||
// TODO: We know runStep is defined, we need a proper normalized annotations type
|
||||
runStep!(label, play, playFunctionContext),
|
||||
};
|
||||
return play(playFunctionContext);
|
||||
});
|
||||
|
||||
return {
|
||||
...partialAnnotations,
|
||||
moduleExport,
|
||||
id,
|
||||
name,
|
||||
story: name,
|
||||
originalStoryFn: render,
|
||||
undecoratedStoryFn,
|
||||
unboundStoryFn,
|
||||
applyLoaders,
|
||||
playFunction,
|
||||
};
|
||||
}
|
||||
|
||||
export function prepareMeta<TRenderer extends Renderer>(
|
||||
componentAnnotations: NormalizedComponentAnnotations<TRenderer>,
|
||||
projectAnnotations: NormalizedProjectAnnotations<TRenderer>,
|
||||
moduleExport: ModuleExport
|
||||
): PreparedMeta<TRenderer> {
|
||||
return {
|
||||
...preparePartialAnnotations(undefined, componentAnnotations, projectAnnotations),
|
||||
moduleExport,
|
||||
};
|
||||
}
|
||||
|
||||
function preparePartialAnnotations<TRenderer extends Renderer>(
|
||||
storyAnnotations: NormalizedStoryAnnotations<TRenderer> | undefined,
|
||||
componentAnnotations: NormalizedComponentAnnotations<TRenderer>,
|
||||
projectAnnotations: NormalizedProjectAnnotations<TRenderer>
|
||||
): Omit<StoryContextForEnhancers<TRenderer>, 'name' | 'story'> {
|
||||
// NOTE: in the current implementation we are doing everything once, up front, rather than doing
|
||||
// anything at render time. The assumption is that as we don't load all the stories at once, this
|
||||
// will have a limited cost. If this proves misguided, we can refactor it.
|
||||
|
||||
const id = storyAnnotations?.id || componentAnnotations.id;
|
||||
|
||||
const tags = [...(storyAnnotations?.tags || componentAnnotations.tags || []), 'story'];
|
||||
|
||||
const parameters: Parameters = combineParameters(
|
||||
projectAnnotations.parameters,
|
||||
componentAnnotations.parameters,
|
||||
storyAnnotations?.parameters
|
||||
);
|
||||
|
||||
// Currently it is only possible to set these globally
|
||||
const { argTypesEnhancers = [], argsEnhancers = [] } = projectAnnotations;
|
||||
|
||||
// The render function on annotations *has* to be an `ArgsStoryFn`, so when we normalize
|
||||
// CSFv1/2, we use a new field called `userStoryFn` so we know that it can be a LegacyStoryFn
|
||||
const render =
|
||||
storyAnnotations?.userStoryFn ||
|
||||
storyAnnotations?.render ||
|
||||
componentAnnotations.render ||
|
||||
projectAnnotations.render;
|
||||
|
||||
if (!render) throw new Error(`No render function available for id '${id}'`);
|
||||
const passedArgTypes: StrictArgTypes = combineParameters(
|
||||
projectAnnotations.argTypes,
|
||||
componentAnnotations.argTypes,
|
||||
@ -128,11 +214,12 @@ function prepareAnnotations<TRenderer extends Renderer>(
|
||||
|
||||
const contextForEnhancers: StoryContextForEnhancers<TRenderer> = {
|
||||
componentId: componentAnnotations.id,
|
||||
title,
|
||||
kind: title, // Back compat
|
||||
id: id || `${componentAnnotations.id}--__meta`,
|
||||
name: name || '__meta',
|
||||
story: name || '__meta', // Back compat
|
||||
title: componentAnnotations.title,
|
||||
kind: componentAnnotations.title, // Back compat
|
||||
id: storyAnnotations?.id || componentAnnotations.id,
|
||||
// if there's no story name, we create a fake one since enhancers expect a name
|
||||
name: storyAnnotations?.name || '__meta',
|
||||
story: storyAnnotations?.name || '__meta', // Back compat
|
||||
component: componentAnnotations.component,
|
||||
subcomponents: componentAnnotations.subcomponents,
|
||||
tags,
|
||||
@ -173,7 +260,7 @@ function prepareAnnotations<TRenderer extends Renderer>(
|
||||
if (!global.FEATURES?.breakingChangesV7) {
|
||||
contextForEnhancers.parameters = {
|
||||
...contextForEnhancers.parameters,
|
||||
__id: id,
|
||||
id,
|
||||
globals: projectAnnotations.globals,
|
||||
globalTypes: projectAnnotations.globalTypes,
|
||||
args: contextForEnhancers.initialArgs,
|
||||
@ -181,69 +268,7 @@ function prepareAnnotations<TRenderer extends Renderer>(
|
||||
};
|
||||
}
|
||||
|
||||
const applyLoaders = async (context: StoryContextForLoaders<TRenderer>) => {
|
||||
const loadResults = await Promise.all(loaders.map((loader) => loader(context)));
|
||||
const loaded = Object.assign({}, ...loadResults);
|
||||
return { ...context, loaded };
|
||||
};
|
||||
const { name, story, ...withoutStoryIdentifiers } = contextForEnhancers;
|
||||
|
||||
const undecoratedStoryFn: LegacyStoryFn<TRenderer> = (context: StoryContext<TRenderer>) => {
|
||||
const mappedArgs = Object.entries(context.args).reduce((acc, [key, val]) => {
|
||||
const mapping = context.argTypes[key]?.mapping;
|
||||
acc[key] = mapping && val in mapping ? mapping[val] : val;
|
||||
return acc;
|
||||
}, {} as Args);
|
||||
|
||||
const includedArgs = Object.entries(mappedArgs).reduce((acc, [key, val]) => {
|
||||
const argType = context.argTypes[key] || {};
|
||||
if (includeConditionalArg(argType, mappedArgs, context.globals)) acc[key] = val;
|
||||
return acc;
|
||||
}, {} as Args);
|
||||
|
||||
const includedContext = { ...context, args: includedArgs };
|
||||
const { passArgsFirst: renderTimePassArgsFirst = true } = context.parameters;
|
||||
return renderTimePassArgsFirst
|
||||
? (render as ArgsStoryFn<TRenderer>)(includedContext.args, includedContext)
|
||||
: (render as LegacyStoryFn<TRenderer>)(includedContext);
|
||||
};
|
||||
const decoratedStoryFn = applyHooks<TRenderer>(applyDecorators)(undecoratedStoryFn, decorators);
|
||||
const unboundStoryFn = (context: StoryContext<TRenderer>) => {
|
||||
let finalContext: StoryContext<TRenderer> = context;
|
||||
if (global.FEATURES?.argTypeTargetsV7) {
|
||||
const argsByTarget = groupArgsByTarget(context);
|
||||
finalContext = {
|
||||
...context,
|
||||
allArgs: context.args,
|
||||
argsByTarget,
|
||||
args: argsByTarget[NO_TARGET_NAME] || {},
|
||||
};
|
||||
}
|
||||
|
||||
return decoratedStoryFn(finalContext);
|
||||
};
|
||||
|
||||
const play = storyAnnotations?.play || componentAnnotations.play;
|
||||
|
||||
const playFunction =
|
||||
play &&
|
||||
(async (storyContext: StoryContext<TRenderer>) => {
|
||||
const playFunctionContext: PlayFunctionContext<TRenderer> = {
|
||||
...storyContext,
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
step: (label: StepLabel, play: PlayFunction<TRenderer>) =>
|
||||
// TODO: We know runStep is defined, we need a proper normalized annotations type
|
||||
runStep!(label, play, playFunctionContext),
|
||||
};
|
||||
return play(playFunctionContext);
|
||||
});
|
||||
|
||||
return Object.freeze({
|
||||
...contextForEnhancers,
|
||||
moduleExport,
|
||||
originalStoryFn: render,
|
||||
undecoratedStoryFn,
|
||||
unboundStoryFn,
|
||||
applyLoaders,
|
||||
playFunction,
|
||||
});
|
||||
return withoutStoryIdentifiers;
|
||||
}
|
||||
|
@ -86,6 +86,13 @@ export type PreparedStory<TRenderer extends Renderer = Renderer> =
|
||||
playFunction?: (context: StoryContext<TRenderer>) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export type PreparedMeta<TRenderer extends Renderer = Renderer> = Omit<
|
||||
StoryContextForEnhancers<TRenderer>,
|
||||
'name' | 'story'
|
||||
> & {
|
||||
moduleExport: ModuleExport;
|
||||
};
|
||||
|
||||
export type BoundStory<TRenderer extends Renderer = Renderer> = PreparedStory<TRenderer> & {
|
||||
storyFn: PartialStoryFn<TRenderer>;
|
||||
};
|
||||
|
@ -30,7 +30,11 @@ export const useOf = (of: Of, validTypes: ResolvedModuleExport['type'][] = []) =
|
||||
case 'meta': {
|
||||
return {
|
||||
...resolved,
|
||||
preparedMeta: prepareMeta(resolved.csfFile.meta, context.projectAnnotations),
|
||||
preparedMeta: prepareMeta(
|
||||
resolved.csfFile.meta,
|
||||
context.projectAnnotations,
|
||||
resolved.csfFile.moduleExports.default
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'story':
|
||||
|
Loading…
x
Reference in New Issue
Block a user