Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support Storybook stories with title but no exported component #1754

Merged
merged 2 commits into from
Jun 23, 2023
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
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