Skip to content

Commit

Permalink
feat: support Storybook stories with title but no exported component (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
fwouts committed Jun 23, 2023
1 parent 1123bbc commit 138fac5
Show file tree
Hide file tree
Showing 19 changed files with 787 additions and 193 deletions.
2 changes: 1 addition & 1 deletion api/src/rpcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,5 @@ export type ComponentInfo =
end: number;
value: SerializableValue;
} | null;
associatedComponentId: string;
associatedComponentId: string | null;
};
2 changes: 1 addition & 1 deletion core/src/detect-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export function detectedComponentToApiComponent(
kind: "story",
args: component.info.args,
associatedComponentId:
component.info.associatedComponent.componentId,
component.info.associatedComponent?.componentId || null,
},
};
}
Expand Down
12 changes: 9 additions & 3 deletions core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
TypeAnalyzer,
ValueType,
} from "@previewjs/type-analyzer";
import { createTypeAnalyzer } from "@previewjs/type-analyzer";
import { createTypeAnalyzer, UNKNOWN_TYPE } from "@previewjs/type-analyzer";
import type { Reader } from "@previewjs/vfs";
import express from "express";
import fs from "fs-extra";
Expand Down Expand Up @@ -116,7 +116,13 @@ export async function createWorkspace({
if (component.info.kind === "component") {
analyze = component.info.analyze;
} else {
analyze = component.info.associatedComponent.analyze;
analyze =
component.info.associatedComponent?.analyze ||
(() =>
Promise.resolve({
propsType: UNKNOWN_TYPE,
types: {},
}));
}
logger.debug(`Analyzing ${component.info.kind}: ${componentId}`);
const { propsType: props, types: componentTypes } = await analyze();
Expand All @@ -132,7 +138,7 @@ export async function createWorkspace({
kind: "story",
args: component.info.args,
associatedComponentId:
component.info.associatedComponent.componentId,
component.info.associatedComponent?.componentId || null,
},
props,
};
Expand Down
2 changes: 1 addition & 1 deletion core/src/plugins/framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export type ComponentTypeInfo =
end: number;
value: SerializableValue;
} | null;
readonly associatedComponent: StoryAssociatedComponent;
readonly associatedComponent: StoryAssociatedComponent | null;
};

export type StoryAssociatedComponent = {
Expand Down
124 changes: 98 additions & 26 deletions framework-plugins/preact/src/extract-component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ export default () => {
expect(extract(APP_TSX)).toMatchObject([]);
});

it("detects CSF1 stories", async () => {
it("detects CSF1 stories (exported with component)", async () => {
memoryReader.updateFile(
APP_STORIES_TSX,
`
Expand Down Expand Up @@ -320,27 +320,62 @@ export const NotStory = (props) => <Button {...props} />;
},
},
]);
if (extractedStories[0]?.info.kind !== "story") {
const storyInfo = extractedStories[0]?.info;
if (storyInfo?.kind !== "story" || !storyInfo.associatedComponent) {
throw new Error();
}
expect(
await extractedStories[0].info.associatedComponent.analyze()
).toEqual({
expect(await storyInfo.associatedComponent.analyze()).toEqual({
propsType: objectType({
label: STRING_TYPE,
}),
types: {},
});
});

it("detects CSF2 stories", async () => {
it("detects CSF1 stories (exported with title)", async () => {
memoryReader.updateFile(
APP_STORIES_TSX,
`
import Button from "./App";
export default {
component: Button
title: "Stories"
}
export const Primary = () => <Button primary label="Button" />;
export const NotStory = (props) => <Button {...props} />;
`
);

const extractedStories = extract(APP_STORIES_TSX);
expect(extractedStories).toMatchObject([
{
componentId: "App.stories.tsx:Primary",
info: {
kind: "story",
args: null,
associatedComponent: null,
},
},
{
componentId: "App.stories.tsx:NotStory",
info: {
kind: "component",
exported: true,
},
},
]);
});

it("detects CSF2 stories (exported with title)", async () => {
memoryReader.updateFile(
APP_STORIES_TSX,
`
import Button from "./App";
export default {
title: "Stories"
}
const Template = (args) => <Button {...args} />;
Expand Down Expand Up @@ -380,26 +415,13 @@ Primary.args = {
},
]),
},
associatedComponent: {
componentId: "App.tsx:default",
},
associatedComponent: null,
},
},
]);
if (extractedStories[1]?.info.kind !== "story") {
throw new Error();
}
expect(
await extractedStories[1].info.associatedComponent.analyze()
).toEqual({
propsType: objectType({
label: STRING_TYPE,
}),
types: {},
});
});

it("detects CSF3 stories", async () => {
it("detects CSF3 stories (exported with component)", async () => {
memoryReader.updateFile(
APP_STORIES_TSX,
`
Expand Down Expand Up @@ -452,19 +474,69 @@ export function NotStory() {}
},
},
]);
if (extractedStories[0]?.info.kind !== "story") {
const storyInfo = extractedStories[0]?.info;
if (storyInfo?.kind !== "story" || !storyInfo.associatedComponent) {
throw new Error();
}
expect(
await extractedStories[0].info.associatedComponent.analyze()
).toEqual({
expect(await storyInfo.associatedComponent.analyze()).toEqual({
propsType: objectType({
label: STRING_TYPE,
}),
types: {},
});
});

it("detects CSF3 stories (exported with title)", async () => {
memoryReader.updateFile(
APP_STORIES_TSX,
`
import Button from "./App";
export default {
title: "Stories"
}
export const Example = {
args: {
label: "Hello, World!"
}
}
export const NoArgs = {}
export function NotStory() {}
`
);

const extractedStories = extract(APP_STORIES_TSX);
expect(extractedStories).toMatchObject([
{
componentId: "App.stories.tsx:Example",
info: {
kind: "story",
args: {
value: object([
{
kind: "key",
key: string("label"),
value: string("Hello, World!"),
},
]),
},
associatedComponent: null,
},
},
{
componentId: "App.stories.tsx:NoArgs",
info: {
kind: "story",
args: null,
associatedComponent: null,
},
},
]);
});

function extract(absoluteFilePath: string) {
return extractPreactComponents(
logger,
Expand Down
18 changes: 7 additions & 11 deletions framework-plugins/preact/src/extract-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { parseSerializableValue } from "@previewjs/serializable-values";
import {
extractArgs,
extractCsf3Stories,
extractDefaultComponent,
extractStoriesInfo,
resolveComponentId,
} from "@previewjs/storybook-helpers";
import { TypeResolver, UNKNOWN_TYPE, helpers } from "@previewjs/type-analyzer";
Expand Down Expand Up @@ -60,7 +60,7 @@ export function extractPreactComponents(
}
}

const storiesDefaultComponent = extractDefaultComponent(sourceFile);
const storiesInfo = extractStoriesInfo(sourceFile);
const components: AnalyzableComponent[] = [];
const args = extractArgs(sourceFile);
const nameToExportedName = helpers.extractExportedNames(sourceFile);
Expand All @@ -69,27 +69,23 @@ export function extractPreactComponents(
node: ts.Node,
name: string
): ComponentTypeInfo | null {
if (name === "default" && storiesDefaultComponent) {
if (name === "default" && storiesInfo) {
return null;
}
const storyArgs = args[name];
const isExported = name === "default" || !!nameToExportedName[name];
const signature = extractComponentSignature(resolver.checker, node);
if (
storiesDefaultComponent &&
storiesInfo &&
isExported &&
(storyArgs || signature?.parameters.length === 0)
) {
const associatedComponent = extractStoryAssociatedComponent(
logger,
resolver,
rootDirPath,
storiesDefaultComponent
storiesInfo.component
);
if (!associatedComponent) {
// No detected associated component, give up.
return null;
}
return {
kind: "story",
args: storyArgs
Expand Down Expand Up @@ -157,14 +153,14 @@ function extractStoryAssociatedComponent(
logger: Logger,
resolver: TypeResolver,
rootDirPath: string,
component: ts.Expression
component: ts.Expression | null
) {
const resolvedStoriesComponentId = resolveComponentId(
rootDirPath,
resolver.checker,
component
);
return resolvedStoriesComponentId
return component && resolvedStoriesComponentId
? {
componentId: resolvedStoriesComponentId,
analyze: async () => {
Expand Down
Loading

0 comments on commit 138fac5

Please sign in to comment.