From 6bd5d3b8af63438ccc58c12ac3577d5e65a3379f Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Wed, 11 Sep 2024 14:12:07 +0200 Subject: [PATCH 1/8] perf: update Storybook to 8.3.0-beta.5 --- package.json | 10 +- packages/storybook-utils/package.json | 4 +- packages/storybook-utils/src/preview.ts | 12 +- .../src/source-code-generator.spec.ts | 262 -------- .../src/source-code-generator.ts | 592 ------------------ pnpm-lock.yaml | 303 ++++++--- 6 files changed, 210 insertions(+), 973 deletions(-) delete mode 100644 packages/storybook-utils/src/source-code-generator.spec.ts delete mode 100644 packages/storybook-utils/src/source-code-generator.ts diff --git a/package.json b/package.json index a01ddd95d..276388759 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,10 @@ "@rushstack/eslint-patch": "^1.10.4", "@sit-onyx/eslint-plugin": "workspace:^", "@sit-onyx/storybook-utils": "workspace:^", - "@storybook/addon-essentials": "^8.2.9", - "@storybook/blocks": "^8.2.9", - "@storybook/vue3": "^8.2.9", - "@storybook/vue3-vite": "^8.2.9", + "@storybook/addon-essentials": "8.3.0-beta.5", + "@storybook/blocks": "8.3.0-beta.5", + "@storybook/vue3": "8.3.0-beta.5", + "@storybook/vue3-vite": "8.3.0-beta.5", "@tsconfig/node20": "^20.1.4", "@types/jsdom": "^21.1.7", "@types/node": "^20.16.5", @@ -51,7 +51,7 @@ "rimraf": "^6.0.1", "sass": "^1.78.0", "simple-git-hooks": "^2.11.1", - "storybook": "^8.2.9", + "storybook": "8.3.0-beta.5", "turbo": "^2.1.1", "typescript": "~5.5.4", "vite": "^5.4.3", diff --git a/packages/storybook-utils/package.json b/packages/storybook-utils/package.json index 830407120..071cea7be 100644 --- a/packages/storybook-utils/package.json +++ b/packages/storybook-utils/package.json @@ -29,9 +29,9 @@ }, "peerDependencies": { "@sit-onyx/icons": "workspace:^", - "@storybook/vue3": ">= 8.2.0", + "@storybook/vue3": ">= 8.3.0-alpha.5", "sit-onyx": "workspace:^", - "storybook": ">= 8.2.0", + "storybook": ">= 8.3.0-alpha.5", "storybook-dark-mode": ">= 4" }, "dependencies": { diff --git a/packages/storybook-utils/src/preview.ts b/packages/storybook-utils/src/preview.ts index 50544d2aa..65d9a0faa 100644 --- a/packages/storybook-utils/src/preview.ts +++ b/packages/storybook-utils/src/preview.ts @@ -1,5 +1,5 @@ import { getIconImportName } from "@sit-onyx/icons"; -import { type Preview, type StoryContext } from "@storybook/vue3"; +import type { Preview } from "@storybook/vue3"; import { deepmerge } from "deepmerge-ts"; import { DARK_MODE_EVENT_NAME } from "storybook-dark-mode"; import { DOCS_RENDERED } from "storybook/internal/core-events"; @@ -7,7 +7,6 @@ import { addons } from "storybook/internal/preview-api"; import type { ThemeVars } from "storybook/internal/theming"; import { enhanceEventArgTypes } from "./actions"; import { requiredGlobalType, withRequired } from "./required"; -import { generateSourceCode } from "./source-code-generator"; import { ONYX_BREAKPOINTS, createTheme } from "./theme"; const themes = { @@ -126,16 +125,15 @@ export const createPreview = (overrides?: T) => { * * @see https://storybook.js.org/docs/react/api/doc-block-source */ -export const sourceCodeTransformer = ( - sourceCode: string, - ctx: Pick, -): string => { +export const sourceCodeTransformer = (originalSourceCode: string): string => { const RAW_ICONS = import.meta.glob("../node_modules/@sit-onyx/icons/src/assets/*.svg", { query: "?raw", import: "default", eager: true, }); + let code = originalSourceCode; + /** * Mapping between icon SVG content (key) and icon name (value). * Needed to display a labelled dropdown list of all available icons. @@ -148,8 +146,6 @@ export const sourceCodeTransformer = ( {}, ); - let code = generateSourceCode(ctx); - const additionalImports: string[] = []; // add icon imports to the source code for all used onyx icons diff --git a/packages/storybook-utils/src/source-code-generator.spec.ts b/packages/storybook-utils/src/source-code-generator.spec.ts deleted file mode 100644 index d820a4533..000000000 --- a/packages/storybook-utils/src/source-code-generator.spec.ts +++ /dev/null @@ -1,262 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -// -// This file is only a temporary copy of the improved source code generation for Storybook. -// It is intended to be deleted once its officially released in Storybook itself, see: -// https://github.com/storybookjs/storybook/pull/27194 -// -import { expect, test } from "vitest"; -import { h } from "vue"; -import { - generatePropsSourceCode, - generateSlotSourceCode, - generateSourceCode, - getFunctionParamNames, - parseDocgenInfo, - type SourceCodeGeneratorContext, -} from "./source-code-generator"; - -test("should generate source code for props", () => { - const ctx: SourceCodeGeneratorContext = { - scriptVariables: {}, - imports: {}, - }; - - const code = generatePropsSourceCode( - { - a: "foo", - b: '"I am double quoted"', - c: 42, - d: true, - e: false, - f: [1, 2, 3], - g: { - g1: "foo", - g2: 42, - }, - h: undefined, - i: null, - j: "", - k: BigInt(9007199254740991), - l: Symbol(), - m: Symbol("foo"), - modelValue: "test-v-model", - otherModelValue: 42, - default: "default slot", - testSlot: "test slot", - }, - ["default", "testSlot"], - ["update:modelValue", "update:otherModelValue"], - ctx, - ); - - expect(code).toBe( - `a="foo" b='"I am double quoted"' :c="42" d :e="false" :f="f" :g="g" :k="BigInt(9007199254740991)" :l="Symbol()" :m="Symbol('foo')" v-model="modelValue" v-model:otherModelValue="otherModelValue"`, - ); - - expect(ctx.scriptVariables).toStrictEqual({ - f: `[1,2,3]`, - g: `{"g1":"foo","g2":42}`, - modelValue: 'ref("test-v-model")', - otherModelValue: "ref(42)", - }); - - expect(Array.from(ctx.imports.vue.values())).toStrictEqual(["ref"]); -}); - -test("should generate source code for slots", () => { - // slot code generator should support primitive values (string, number etc.) - // but also VNodes (e.g. created using h()) so custom Vue components can also be used - // inside slots with proper generated code - - const slots = { - default: "default content", - a: "a content", - b: 42, - c: true, - // single VNode without props - d: h("div", "d content"), - // VNode with props and single child - e: h("div", { style: "color:red" }, "e content"), - // VNode with props and single child returned as getter - f: h("div", { style: "color:red" }, () => "f content"), - // VNode with multiple children - g: h("div", { style: "color:red" }, [ - "child 1", - h("span", { style: "color:green" }, "child 2"), - ]), - // VNode multiple children but returned as getter - h: h("div", { style: "color:red" }, () => [ - "child 1", - h("span", { style: "color:green" }, "child 2"), - ]), - // VNode with multiple and nested children - i: h("div", { style: "color:red" }, [ - "child 1", - h("span", { style: "color:green" }, ["nested child 1", h("p", "nested child 2")]), - ]), - j: ["child 1", "child 2"], - k: null, - l: { foo: "bar" }, - m: BigInt(9007199254740991), - }; - - const expectedCode = `default content - - - - - - - - - - - - - - - - - - - - - - - -`; - - let actualCode = generateSlotSourceCode(slots, Object.keys(slots), { - scriptVariables: {}, - imports: {}, - }); - expect(actualCode).toBe(expectedCode); - - // should generate the same code if getters/functions are used to return the slot content - const slotsWithGetters = Object.entries(slots).reduce< - Record (typeof slots)[keyof typeof slots]> - >((obj, [slotName, value]) => { - obj[slotName] = () => value; - return obj; - }, {}); - - actualCode = generateSlotSourceCode(slotsWithGetters, Object.keys(slotsWithGetters), { - scriptVariables: {}, - imports: {}, - }); - expect(actualCode).toBe(expectedCode); -}); - -test("should generate source code for slots with bindings", () => { - type TestBindings = { - foo: string; - bar?: number; - boo: { - mimeType: string; - }; - }; - - const slots = { - a: ({ foo, bar, boo }: TestBindings) => `Slot with bindings ${foo}, ${bar} and ${boo.mimeType}`, - b: ({ foo, boo }: TestBindings) => - h("a", { href: foo, target: foo, type: boo.mimeType, ...boo }, `Test link: ${foo}`), - }; - - const expectedCode = ` - -`; - - const actualCode = generateSlotSourceCode(slots, Object.keys(slots), { - imports: {}, - scriptVariables: {}, - }); - expect(actualCode).toBe(expectedCode); -}); - -test("should generate source code with - -`); -}); - -test.each([ - { __docgenInfo: "invalid-value", slotNames: [] }, - { __docgenInfo: {}, slotNames: [] }, - { __docgenInfo: { slots: "invalid-value" }, slotNames: [] }, - { __docgenInfo: { slots: ["invalid-value"] }, slotNames: [] }, - { - __docgenInfo: { slots: [{ name: "slot-1" }, { name: "slot-2" }, { notName: "slot-3" }] }, - slotNames: ["slot-1", "slot-2"], - }, -])("should parse slots names from __docgenInfo", ({ __docgenInfo, slotNames }) => { - const docgenInfo = parseDocgenInfo({ __docgenInfo }); - expect(docgenInfo.slotNames).toStrictEqual(slotNames); -}); - -test.each([ - { __docgenInfo: "invalid-value", eventNames: [] }, - { __docgenInfo: {}, eventNames: [] }, - { __docgenInfo: { events: "invalid-value" }, eventNames: [] }, - { __docgenInfo: { events: ["invalid-value"] }, eventNames: [] }, - { - __docgenInfo: { events: [{ name: "event-1" }, { name: "event-2" }, { notName: "event-3" }] }, - eventNames: ["event-1", "event-2"], - }, -])("should parse event names from __docgenInfo", ({ __docgenInfo, eventNames }) => { - const docgenInfo = parseDocgenInfo({ __docgenInfo }); - expect(docgenInfo.eventNames).toStrictEqual(eventNames); -}); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -test.each<{ fn: (...args: any[]) => unknown; expectedNames: string[] }>([ - { fn: () => ({}), expectedNames: [] }, - { fn: (a) => ({}), expectedNames: ["a"] }, - { fn: (a, b) => ({}), expectedNames: ["a", "b"] }, - { fn: (a, b, { c }) => ({}), expectedNames: ["a", "b", "{", "c", "}"] }, - { fn: ({ a, b }) => ({}), expectedNames: ["{", "a", "b", "}"] }, - { - fn: { - // simulate minified function after running "storybook build" - toString: () => "({a:foo,b:bar})=>({})", - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as (...args: any[]) => unknown, - expectedNames: ["{", "a", "b", "}"], - }, -])("should extract function parameter names", ({ fn, expectedNames }) => { - const paramNames = getFunctionParamNames(fn); - expect(paramNames).toStrictEqual(expectedNames); -}); diff --git a/packages/storybook-utils/src/source-code-generator.ts b/packages/storybook-utils/src/source-code-generator.ts deleted file mode 100644 index 072878212..000000000 --- a/packages/storybook-utils/src/source-code-generator.ts +++ /dev/null @@ -1,592 +0,0 @@ -// -// This file is only a temporary copy of the improved source code generation for Storybook. -// It is intended to be deleted once its officially released in Storybook itself, see: -// https://github.com/storybookjs/storybook/pull/27194 -// -import type { Args, StoryContext } from "@storybook/vue3"; -import { SourceType } from "storybook/internal/docs-tools"; -import { isVNode, type VNode } from "vue"; -import { replaceAll } from "./preview"; - -/** - * Used to get the tracking data from the proxy. - * A symbol is unique, so when using it as a key it can't be accidentally accessed. - */ -const TRACKING_SYMBOL = Symbol("DEEP_ACCESS_SYMBOL"); - -type TrackingProxy = { - [TRACKING_SYMBOL]: true; - toString: () => string; -}; - -const isProxy = (obj: unknown): obj is TrackingProxy => - !!(obj && typeof obj === "object" && TRACKING_SYMBOL in obj); - -/** - * Context that is passed down to nested components/slots when generating the source code for a single story. - */ -export type SourceCodeGeneratorContext = { - /** - * Properties/variables that should be placed inside a ` - -${template}`; -}; - -/** - * Checks if the source code generation should be skipped for the given Story context. - * Will be true if one of the following is true: - * - view mode is not "docs" - * - story is no arg story - * - story has set custom source code via parameters.docs.source.code - * - story has set source type to "code" via parameters.docs.source.type - */ -export const shouldSkipSourceCodeGeneration = (context: StoryContext): boolean => { - const sourceParams = context?.parameters.docs?.source; - if (sourceParams?.type === SourceType.DYNAMIC) { - // always render if the user forces it - return false; - } - - const isArgsStory = context?.parameters.__isArgsStory; - const isDocsViewMode = context?.viewMode === "docs"; - - // never render if the user is forcing the block to render code, or - // if the user provides code, or if it's not an args story. - return ( - !isDocsViewMode || !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE - ); -}; - -/** - * Parses the __docgenInfo of the given component. - * Requires Storybook docs addon to be enabled. - * Default slot will always be sorted first, remaining slots are sorted alphabetically. - */ -export const parseDocgenInfo = ( - component?: StoryContext["component"] & { __docgenInfo?: unknown }, -) => { - // type check __docgenInfo to prevent errors - if ( - !component || - !("__docgenInfo" in component) || - !component.__docgenInfo || - typeof component.__docgenInfo !== "object" - ) { - return { - displayName: component?.__name, - eventNames: [], - slotNames: [], - }; - } - - const docgenInfo = component.__docgenInfo as Record; - - const displayName = - "displayName" in docgenInfo && typeof docgenInfo.displayName === "string" - ? docgenInfo.displayName - : undefined; - - const parseNames = (key: "slots" | "events") => { - if (!(key in docgenInfo) || !Array.isArray(docgenInfo[key])) return []; - - const values = docgenInfo[key] as unknown[]; - - return values - .map((i) => (i && typeof i === "object" && "name" in i ? i.name : undefined)) - .filter((i): i is string => typeof i === "string"); - }; - - return { - displayName: displayName || component.__name, - slotNames: parseNames("slots").sort((a, b) => { - if (a === "default") return -1; - if (b === "default") return 1; - return a.localeCompare(b); - }), - eventNames: parseNames("events"), - }; -}; - -/** - * Generates the source code for the given Vue component properties. - * Props with complex values (objects and arrays) and v-models will be added to the ctx.scriptVariables because they should be - * generated in a `