From 949e057f7be228ff41d9bafafb49049b2f29dcdd Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 3 Mar 2023 16:55:42 +1100 Subject: [PATCH 1/7] Working on getting ArgsTable working unattached --- code/ui/blocks/src/blocks/ArgsTable.tsx | 27 ++++++++++++++----------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/code/ui/blocks/src/blocks/ArgsTable.tsx b/code/ui/blocks/src/blocks/ArgsTable.tsx index acfcf0414d4d..b01c49da46ac 100644 --- a/code/ui/blocks/src/blocks/ArgsTable.tsx +++ b/code/ui/blocks/src/blocks/ArgsTable.tsx @@ -4,7 +4,7 @@ import mapValues from 'lodash/mapValues.js'; import type { ArgTypesExtractor } from '@storybook/docs-tools'; import type { PropDescriptor } from '@storybook/preview-api'; import { filterArgTypes } from '@storybook/preview-api'; -import type { StrictArgTypes, Args, Globals } from '@storybook/types'; +import type { StrictArgTypes, Args, Globals, Parameters } from '@storybook/types'; import { STORY_ARGS_UPDATED, UPDATE_STORY_ARGS, @@ -90,11 +90,10 @@ const useGlobals = (context: DocsContextProps): [Globals] => { export const extractComponentArgTypes = ( component: Component, - context: DocsContextProps, + parameters: Parameters, include?: PropDescriptor, exclude?: PropDescriptor ): StrictArgTypes => { - const { parameters } = context.storyById(); const { extractArgTypes }: { extractArgTypes: ArgTypesExtractor } = parameters.docs || {}; if (!extractArgTypes) { throw new Error(ArgsTableError.ARGS_UNSUPPORTED); @@ -109,10 +108,9 @@ const isShortcut = (value?: string) => { return value && [PRIMARY_STORY].includes(value); }; -export const getComponent = (props: ArgsTableProps = {}, context: DocsContextProps): Component => { +export const getComponent = (props: ArgsTableProps = {}, component: Component): Component => { const { of } = props as OfProps; const { story } = props as StoryProps; - const { component } = context.storyById(); if (isShortcut(of) || isShortcut(story)) { return component || null; } @@ -220,17 +218,22 @@ export const ArgsTable: FC = (props) => { Please refer to the migration guide: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#argstable-block `); const context = useContext(DocsContext); - const { - parameters: { controls }, - subcomponents, - } = context.storyById(); + + let primaryStory; + try { + primaryStory = context.storyById(); + } catch (err) { + // It is OK to use the ArgsTable unattached, we don't have this information + } + + const { parameters, component, subcomponents } = primaryStory || { parameters: {} as Parameters }; const { include, exclude, components, sort: sortProp } = props as ComponentsProps; const { story: storyName } = props as StoryProps; - const sort = sortProp || controls?.sort; + const sort = sortProp || parameters.controls?.sort; - const main = getComponent(props, context); + const main = getComponent(props, component); if (storyName) { return ; } @@ -238,7 +241,7 @@ export const ArgsTable: FC = (props) => { if (!components && !subcomponents) { let mainProps; try { - mainProps = { rows: extractComponentArgTypes(main, context, include, exclude) }; + mainProps = { rows: extractComponentArgTypes(main, parameters, include, exclude) }; } catch (err) { mainProps = { error: err.message }; } From d5a3ba2c22d74c94a054533f9bd980cec498c881 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 3 Mar 2023 17:15:28 +1100 Subject: [PATCH 2/7] Refactor useOf to do the extra bit inside the docs context --- .../preview-web/docs-context/DocsContext.ts | 26 +++++++++- code/lib/types/src/modules/docs.ts | 22 +++++---- code/ui/blocks/src/blocks/useOf.ts | 47 ++----------------- 3 files changed, 42 insertions(+), 53 deletions(-) diff --git a/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.ts b/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.ts index 3542e0a1919c..eca4a615e53f 100644 --- a/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.ts +++ b/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.ts @@ -9,10 +9,12 @@ import type { StoryName, ResolvedModuleExportType, ResolvedModuleExportFromType, + EnhancedResolvedModuleExportType, } from '@storybook/types'; import type { Channel } from '@storybook/channels'; import type { StoryStore } from '../../store'; +import { prepareMeta } from '../../store'; import type { DocsContextProps } from './DocsContextProps'; export class DocsContext implements DocsContextProps { @@ -165,7 +167,29 @@ export class DocsContext implements DocsContextProps )}` ); } - return resolved; + + switch (resolved.type) { + case 'component': { + return { + ...resolved, + projectAnnotations: this.projectAnnotations, + } as EnhancedResolvedModuleExportType; + } + case 'meta': { + return { + ...resolved, + preparedMeta: prepareMeta( + resolved.csfFile.meta, + this.projectAnnotations, + resolved.csfFile.moduleExports.default + ), + } as EnhancedResolvedModuleExportType; + } + case 'story': + default: { + return resolved as EnhancedResolvedModuleExportType; + } + } } storyIdByName = (storyName: StoryName) => { diff --git a/code/lib/types/src/modules/docs.ts b/code/lib/types/src/modules/docs.ts index 831b4905be03..abf183cf543f 100644 --- a/code/lib/types/src/modules/docs.ts +++ b/code/lib/types/src/modules/docs.ts @@ -7,6 +7,7 @@ import type { PreparedStory, NormalizedProjectAnnotations, RenderContext, + PreparedMeta, } from './story'; export type RenderContextCallbacks = Pick< @@ -21,14 +22,6 @@ export type StoryRenderOptions = { export type ResolvedModuleExportType = 'component' | 'meta' | 'story'; -export type ResolvedModuleExportFromType< - TType extends ResolvedModuleExportType, - TRenderer extends Renderer = Renderer -> = TType extends 'component' - ? { type: 'component'; component: TRenderer['component'] } - : TType extends 'meta' - ? { type: 'meta'; csfFile: CSFFile } - : { type: 'story'; story: PreparedStory }; /** * What do we know about an of={} call? * @@ -37,6 +30,19 @@ export type ResolvedModuleExportFromType< * - story === `PreparedStory` * But these shorthands capture the idea of what is being talked about */ +export type ResolvedModuleExportFromType< + TType extends ResolvedModuleExportType, + TRenderer extends Renderer = Renderer +> = TType extends 'component' + ? { + type: 'component'; + component: TRenderer['component']; + projectAnnotations: NormalizedProjectAnnotations; + } + : TType extends 'meta' + ? { type: 'meta'; csfFile: CSFFile; preparedMeta: PreparedMeta } + : { type: 'story'; story: PreparedStory }; + export type ResolvedModuleExport = { type: ResolvedModuleExportType; } & ( diff --git a/code/ui/blocks/src/blocks/useOf.ts b/code/ui/blocks/src/blocks/useOf.ts index fc4ea2cdd3bd..63d5f1e38edb 100644 --- a/code/ui/blocks/src/blocks/useOf.ts +++ b/code/ui/blocks/src/blocks/useOf.ts @@ -1,65 +1,24 @@ import type { DocsContextProps, ModuleExport, - NormalizedProjectAnnotations, - PreparedMeta, Renderer, - ResolvedModuleExportFromType, ResolvedModuleExportType, } from '@storybook/types'; -import { prepareMeta } from '@storybook/preview-api'; import { useContext } from 'react'; import { DocsContext } from './DocsContext'; export type Of = Parameters[0]; -export type EnhancedResolvedModuleExportType< - TType extends ResolvedModuleExportType, - TRenderer extends Renderer = Renderer -> = TType extends 'component' - ? ResolvedModuleExportFromType & { - projectAnnotations: NormalizedProjectAnnotations; - } - : TType extends 'meta' - ? ResolvedModuleExportFromType & { preparedMeta: PreparedMeta } - : ResolvedModuleExportFromType; - /** * A hook to resolve the `of` prop passed to a block. * will return the resolved module * if the resolved module is a meta it will include a preparedMeta property similar to a preparedStory * if the resolved module is a component it will include the project annotations */ -export const useOf = < - TType extends ResolvedModuleExportType, - TRenderer extends Renderer = Renderer ->( +export const useOf = ( moduleExportOrType: ModuleExport | TType, validTypes?: TType[] -): EnhancedResolvedModuleExportType => { +): ReturnType => { const context = useContext(DocsContext); - const resolved = context.resolveOf(moduleExportOrType, validTypes); - - switch (resolved.type) { - case 'component': { - return { - ...resolved, - projectAnnotations: context.projectAnnotations, - } as EnhancedResolvedModuleExportType; - } - case 'meta': { - return { - ...resolved, - preparedMeta: prepareMeta( - resolved.csfFile.meta, - context.projectAnnotations, - resolved.csfFile.moduleExports.default - ), - } as EnhancedResolvedModuleExportType; - } - case 'story': - default: { - return resolved as EnhancedResolvedModuleExportType; - } - } + return context.resolveOf(moduleExportOrType, validTypes); }; From 0a1fd7efc5f72d146cf7a303a4a26ce1c1959368 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 3 Mar 2023 17:20:23 +1100 Subject: [PATCH 3/7] Use `resolveOf` to get global parameters in the ArgsTable --- code/ui/blocks/src/blocks/ArgsTable.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/code/ui/blocks/src/blocks/ArgsTable.tsx b/code/ui/blocks/src/blocks/ArgsTable.tsx index b01c49da46ac..378610a8a40b 100644 --- a/code/ui/blocks/src/blocks/ArgsTable.tsx +++ b/code/ui/blocks/src/blocks/ArgsTable.tsx @@ -219,15 +219,18 @@ export const ArgsTable: FC = (props) => { `); const context = useContext(DocsContext); - let primaryStory; + let parameters: Parameters; + let component: any; + let subcomponents: Record; try { - primaryStory = context.storyById(); + ({ parameters, component, subcomponents } = context.storyById()); } catch (err) { - // It is OK to use the ArgsTable unattached, we don't have this information + const { of } = props as OfProps; + ({ + projectAnnotations: { parameters }, + } = context.resolveOf(of, ['component'])); } - const { parameters, component, subcomponents } = primaryStory || { parameters: {} as Parameters }; - const { include, exclude, components, sort: sortProp } = props as ComponentsProps; const { story: storyName } = props as StoryProps; From c6ba7e124b06bab95de4190ebc1dfaf8ea7a9887 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 3 Mar 2023 19:16:05 +1100 Subject: [PATCH 4/7] Fix types --- .../src/modules/preview-web/docs-context/DocsContext.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.ts b/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.ts index eca4a615e53f..7b33d5429ad1 100644 --- a/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.ts +++ b/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.ts @@ -9,7 +9,6 @@ import type { StoryName, ResolvedModuleExportType, ResolvedModuleExportFromType, - EnhancedResolvedModuleExportType, } from '@storybook/types'; import type { Channel } from '@storybook/channels'; @@ -173,7 +172,7 @@ export class DocsContext implements DocsContextProps return { ...resolved, projectAnnotations: this.projectAnnotations, - } as EnhancedResolvedModuleExportType; + }; } case 'meta': { return { @@ -183,11 +182,11 @@ export class DocsContext implements DocsContextProps this.projectAnnotations, resolved.csfFile.moduleExports.default ), - } as EnhancedResolvedModuleExportType; + }; } case 'story': default: { - return resolved as EnhancedResolvedModuleExportType; + return resolved; } } } From ce9bd7f09485e7bb6686bd25eb6951594acb7c46 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 3 Mar 2023 19:34:39 +1100 Subject: [PATCH 5/7] Fix linting --- code/ui/blocks/src/blocks/useOf.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/code/ui/blocks/src/blocks/useOf.ts b/code/ui/blocks/src/blocks/useOf.ts index 63d5f1e38edb..cde765f061ac 100644 --- a/code/ui/blocks/src/blocks/useOf.ts +++ b/code/ui/blocks/src/blocks/useOf.ts @@ -1,9 +1,4 @@ -import type { - DocsContextProps, - ModuleExport, - Renderer, - ResolvedModuleExportType, -} from '@storybook/types'; +import type { DocsContextProps, ModuleExport, ResolvedModuleExportType } from '@storybook/types'; import { useContext } from 'react'; import { DocsContext } from './DocsContext'; From ade8805296f0ce629393651734ffd4c0ea9096ca Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 6 Mar 2023 09:49:17 +1100 Subject: [PATCH 6/7] Fix types of `useOf` --- code/ui/blocks/src/blocks/Canvas.tsx | 4 ++-- code/ui/blocks/src/blocks/useOf.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/code/ui/blocks/src/blocks/Canvas.tsx b/code/ui/blocks/src/blocks/Canvas.tsx index 77a2fbc6fff2..d354fe775ad3 100644 --- a/code/ui/blocks/src/blocks/Canvas.tsx +++ b/code/ui/blocks/src/blocks/Canvas.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/destructuring-assignment */ import React, { Children, useContext } from 'react'; import type { FC, ReactElement, ReactNode } from 'react'; -import type { ModuleExport, ModuleExports, Renderer } from '@storybook/types'; +import type { ModuleExport, ModuleExports, PreparedStory, Renderer } from '@storybook/types'; import { deprecate } from '@storybook/client-logger'; import dedent from 'ts-dedent'; import type { Layout, PreviewProps as PurePreviewProps } from '../components'; @@ -157,7 +157,7 @@ export const Canvas: FC = (props) => { const { children, of, source } = props; const { isLoading, previewProps } = useDeprecatedPreviewProps(props, docsContext, sourceContext); - let story; + let story: PreparedStory; let sourceProps; /** * useOf and useSourceProps will throw if they can't find the story, in the scenario where diff --git a/code/ui/blocks/src/blocks/useOf.ts b/code/ui/blocks/src/blocks/useOf.ts index cde765f061ac..8e5d9d5f7a4f 100644 --- a/code/ui/blocks/src/blocks/useOf.ts +++ b/code/ui/blocks/src/blocks/useOf.ts @@ -1,4 +1,9 @@ -import type { DocsContextProps, ModuleExport, ResolvedModuleExportType } from '@storybook/types'; +import type { + DocsContextProps, + ModuleExport, + ResolvedModuleExportType, + ResolvedModuleExportFromType, +} from '@storybook/types'; import { useContext } from 'react'; import { DocsContext } from './DocsContext'; @@ -13,7 +18,7 @@ export type Of = Parameters[0]; export const useOf = ( moduleExportOrType: ModuleExport | TType, validTypes?: TType[] -): ReturnType => { +): ResolvedModuleExportFromType => { const context = useContext(DocsContext); return context.resolveOf(moduleExportOrType, validTypes); }; From 47efd803f54ee285669a19a36e3217e0dbc47d48 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 6 Mar 2023 10:10:26 +1100 Subject: [PATCH 7/7] Update DocsContext tests --- .../docs-context/DocsContext.test.ts | 72 ++++++++++++++++--- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.test.ts b/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.test.ts index 79310a0219d3..8988307cf0d0 100644 --- a/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.test.ts +++ b/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.test.ts @@ -36,8 +36,10 @@ describe('resolveOf', () => { const { story, csfFile, storyExport, metaExport, moduleExports, component } = csfFileParts(); describe('attached', () => { + const projectAnnotations = { render: jest.fn() }; const store = { componentStoriesFromCSFFile: () => [story], + projectAnnotations, } as unknown as StoryStore; const context = new DocsContext(channel, store, renderStoryToElement, [csfFile]); context.attachCSFFile(csfFile); @@ -47,15 +49,27 @@ describe('resolveOf', () => { }); it('works for meta exports', () => { - expect(context.resolveOf(metaExport)).toEqual({ type: 'meta', csfFile }); + expect(context.resolveOf(metaExport)).toEqual({ + type: 'meta', + csfFile, + preparedMeta: expect.any(Object), + }); }); it('works for full module exports', () => { - expect(context.resolveOf(moduleExports)).toEqual({ type: 'meta', csfFile }); + expect(context.resolveOf(moduleExports)).toEqual({ + type: 'meta', + csfFile, + preparedMeta: expect.any(Object), + }); }); it('works for components', () => { - expect(context.resolveOf(component)).toEqual({ type: 'component', component }); + expect(context.resolveOf(component)).toEqual({ + type: 'component', + component, + projectAnnotations: expect.objectContaining(projectAnnotations), + }); }); it('finds primary story', () => { @@ -63,11 +77,19 @@ describe('resolveOf', () => { }); it('finds attached CSF file', () => { - expect(context.resolveOf('meta')).toEqual({ type: 'meta', csfFile }); + expect(context.resolveOf('meta')).toEqual({ + type: 'meta', + csfFile, + preparedMeta: expect.any(Object), + }); }); it('finds attached component', () => { - expect(context.resolveOf('component')).toEqual({ type: 'component', component }); + expect(context.resolveOf('component')).toEqual({ + type: 'component', + component, + projectAnnotations: expect.objectContaining(projectAnnotations), + }); }); describe('validation allowed', () => { @@ -76,17 +98,26 @@ describe('resolveOf', () => { }); it('works for meta exports', () => { - expect(context.resolveOf(metaExport, ['meta'])).toEqual({ type: 'meta', csfFile }); + expect(context.resolveOf(metaExport, ['meta'])).toEqual({ + type: 'meta', + csfFile, + preparedMeta: expect.any(Object), + }); }); it('works for full module exports', () => { - expect(context.resolveOf(moduleExports, ['meta'])).toEqual({ type: 'meta', csfFile }); + expect(context.resolveOf(moduleExports, ['meta'])).toEqual({ + type: 'meta', + csfFile, + preparedMeta: expect.any(Object), + }); }); it('works for components', () => { expect(context.resolveOf(component, ['component'])).toEqual({ type: 'component', component, + projectAnnotations: expect.objectContaining(projectAnnotations), }); }); @@ -95,13 +126,18 @@ describe('resolveOf', () => { }); it('finds attached CSF file', () => { - expect(context.resolveOf('meta', ['meta'])).toEqual({ type: 'meta', csfFile }); + expect(context.resolveOf('meta', ['meta'])).toEqual({ + type: 'meta', + csfFile, + preparedMeta: expect.any(Object), + }); }); it('finds attached component', () => { expect(context.resolveOf('component', ['component'])).toEqual({ type: 'component', component, + projectAnnotations: expect.objectContaining(projectAnnotations), }); }); }); @@ -140,8 +176,10 @@ describe('resolveOf', () => { }); describe('unattached', () => { + const projectAnnotations = { render: jest.fn() }; const store = { componentStoriesFromCSFFile: () => [story], + projectAnnotations, } as unknown as StoryStore; const context = new DocsContext(channel, store, renderStoryToElement, [csfFile]); @@ -150,15 +188,27 @@ describe('resolveOf', () => { }); it('works for meta exports', () => { - expect(context.resolveOf(metaExport)).toEqual({ type: 'meta', csfFile }); + expect(context.resolveOf(metaExport)).toEqual({ + type: 'meta', + csfFile, + preparedMeta: expect.any(Object), + }); }); it('works for full module exports', () => { - expect(context.resolveOf(moduleExports)).toEqual({ type: 'meta', csfFile }); + expect(context.resolveOf(moduleExports)).toEqual({ + type: 'meta', + csfFile, + preparedMeta: expect.any(Object), + }); }); it('works for components', () => { - expect(context.resolveOf(component)).toEqual({ type: 'component', component }); + expect(context.resolveOf(component)).toEqual({ + type: 'component', + component, + projectAnnotations: expect.objectContaining(projectAnnotations), + }); }); it('throws for primary story', () => {