@sit-onyx/storybook-utils 1.0.0-beta.5 → 1.0.0-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./actions";
2
2
  export * from "./preview";
3
+ export * from "./sbType";
3
4
  export * from "./theme";
4
5
  export * from "./types";
@@ -1,25 +1,19 @@
1
1
  import bellRing from "@sit-onyx/icons/bell-ring.svg?raw";
2
2
  import calendar from "@sit-onyx/icons/calendar.svg?raw";
3
3
  import placeholder from "@sit-onyx/icons/placeholder.svg?raw";
4
- import { describe, expect, test, vi } from "vitest";
4
+ import { describe, expect, test } from "vitest";
5
5
  import { replaceAll, sourceCodeTransformer } from "./preview";
6
- import * as sourceCodeGenerator from "./source-code-generator";
7
6
 
8
7
  describe("preview.ts", () => {
9
8
  test("should transform source code and add icon/onyx imports", () => {
10
- // ARRANGE
11
- const generatorSpy = vi.spyOn(sourceCodeGenerator, "generateSourceCode")
12
- .mockReturnValue(`<template>
9
+ // ACT
10
+ const sourceCode = sourceCodeTransformer(`<template>
13
11
  <OnyxTest icon='${placeholder}' test='${bellRing}' :obj="{foo:'${replaceAll(calendar, '"', "\\'")}'}" />
14
12
  <OnyxOtherComponent />
15
13
  <OnyxComp>Test</OnyxComp>
16
14
  </template>`);
17
15
 
18
- // ACT
19
- const sourceCode = sourceCodeTransformer("", { title: "OnyxTest", args: {} });
20
-
21
16
  // ASSERT
22
- expect(generatorSpy).toHaveBeenCalledOnce();
23
17
  expect(sourceCode).toBe(`<script lang="ts" setup>
24
18
  import { OnyxComp, OnyxOtherComponent, OnyxTest } from "sit-onyx";
25
19
  import bellRing from "@sit-onyx/icons/bell-ring.svg?raw";
package/src/preview.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { getIconImportName } from "@sit-onyx/icons";
2
- import { type Preview, type StoryContext } from "@storybook/vue3";
2
+ import type { Preview } from "@storybook/vue3";
3
3
  import { deepmerge } from "deepmerge-ts";
4
4
  import { DARK_MODE_EVENT_NAME } from "storybook-dark-mode";
5
5
  import { DOCS_RENDERED } from "storybook/internal/core-events";
6
6
  import { addons } from "storybook/internal/preview-api";
7
7
  import type { ThemeVars } from "storybook/internal/theming";
8
+ import { enhanceEventArgTypes } from "./actions";
8
9
  import { requiredGlobalType, withRequired } from "./required";
9
- import { generateSourceCode } from "./source-code-generator";
10
10
  import { ONYX_BREAKPOINTS, createTheme } from "./theme";
11
11
 
12
12
  const themes = {
@@ -21,6 +21,7 @@ const themes = {
21
21
  * - Setup for dark mode (including docs page). Requires addon `storybook-dark-mode` to be enabled in .storybook/main.ts file
22
22
  * - Custom Storybook theme using onyx colors (light and dark mode)
23
23
  * - Configure viewports / breakpoints as defined by onyx
24
+ * - Logs Vue emits as Storybook events
24
25
  *
25
26
  * @param overrides Custom preview / overrides, will be deep merged with the default preview.
26
27
  *
@@ -41,6 +42,7 @@ const themes = {
41
42
  */
42
43
  export const createPreview = <T extends Preview = Preview>(overrides?: T) => {
43
44
  const defaultPreview = {
45
+ argTypesEnhancers: [enhanceEventArgTypes],
44
46
  globalTypes: {
45
47
  ...requiredGlobalType,
46
48
  },
@@ -123,16 +125,15 @@ export const createPreview = <T extends Preview = Preview>(overrides?: T) => {
123
125
  *
124
126
  * @see https://storybook.js.org/docs/react/api/doc-block-source
125
127
  */
126
- export const sourceCodeTransformer = (
127
- sourceCode: string,
128
- ctx: Pick<StoryContext, "title" | "component" | "args">,
129
- ): string => {
128
+ export const sourceCodeTransformer = (originalSourceCode: string): string => {
130
129
  const RAW_ICONS = import.meta.glob("../node_modules/@sit-onyx/icons/src/assets/*.svg", {
131
130
  query: "?raw",
132
131
  import: "default",
133
132
  eager: true,
134
133
  });
135
134
 
135
+ let code = originalSourceCode;
136
+
136
137
  /**
137
138
  * Mapping between icon SVG content (key) and icon name (value).
138
139
  * Needed to display a labelled dropdown list of all available icons.
@@ -145,8 +146,6 @@ export const sourceCodeTransformer = (
145
146
  {},
146
147
  );
147
148
 
148
- let code = generateSourceCode(ctx);
149
-
150
149
  const additionalImports: string[] = [];
151
150
 
152
151
  // add icon imports to the source code for all used onyx icons
@@ -156,7 +155,10 @@ export const sourceCodeTransformer = (
156
155
  const escapedIconContent = `"${replaceAll(iconContent, '"', '\\"')}"`;
157
156
 
158
157
  if (code.includes(iconContent)) {
159
- code = code.replace(new RegExp(` (\\S+)=['"]${iconContent}['"]`), ` :$1="${importName}"`);
158
+ code = code.replace(
159
+ new RegExp(` (\\S+)=['"]${escapeRegExp(iconContent)}['"]`),
160
+ ` :$1="${importName}"`,
161
+ );
160
162
  additionalImports.push(`import ${importName} from "@sit-onyx/icons/${iconName}.svg?raw";`);
161
163
  } else if (code.includes(singleQuotedIconContent)) {
162
164
  // support icons inside objects
@@ -206,3 +208,11 @@ ${code}`;
206
208
  export const replaceAll = (value: string, searchValue: string | RegExp, replaceValue: string) => {
207
209
  return value.replace(new RegExp(searchValue, "gi"), replaceValue);
208
210
  };
211
+
212
+ /**
213
+ * Escapes the given string value to be used in `new RegExp()`.
214
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping
215
+ */
216
+ export const escapeRegExp = (string: string) => {
217
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
218
+ };
@@ -0,0 +1,38 @@
1
+ import type { SBType } from "storybook/internal/types";
2
+ import { describe, expect, test, vi } from "vitest";
3
+ import { walkTree } from "./sbType";
4
+
5
+ describe("walkTree", () => {
6
+ test.each<{ input: SBType; expected: SBType["name"][] }>([
7
+ { input: { name: "array", value: { name: "number" } }, expected: ["array", "number"] },
8
+ { input: { name: "object", value: { a: { name: "number" } } }, expected: ["object", "number"] },
9
+ { input: { name: "enum", value: ["a"] }, expected: ["enum"] },
10
+ {
11
+ input: { name: "intersection", value: [{ name: "number" }] },
12
+ expected: ["intersection", "number"],
13
+ },
14
+ { input: { name: "union", value: [{ name: "number" }] }, expected: ["union", "number"] },
15
+ { input: { name: "other", value: "a" }, expected: ["other"] },
16
+ ])("should execute cb for $input.name correctly", ({ input, expected }) => {
17
+ const spy = vi.fn<(p: SBType) => void>();
18
+ const result = walkTree(input, spy);
19
+
20
+ expect(result).toBeUndefined();
21
+ expect(spy).toHaveBeenCalledTimes(expected.length);
22
+ const nameCalls = spy.mock.calls.map(([{ name }]) => name);
23
+ expect(nameCalls).toMatchObject(expected);
24
+ });
25
+
26
+ test("should return value if there is any returned", () => {
27
+ const target: SBType = { name: "number", raw: "here" };
28
+ const overshoot: SBType = { name: "boolean", raw: "here" };
29
+ const parent: SBType = { name: "intersection", value: [target, overshoot] };
30
+ const returned = 42;
31
+ const spy = vi.fn((p: SBType) => (p.raw === "here" ? returned : undefined));
32
+ const result = walkTree({ name: "union", value: [parent] }, spy);
33
+
34
+ expect(spy).toHaveBeenCalledTimes(3);
35
+ expect(spy).toHaveBeenLastCalledWith(target, parent);
36
+ expect(result).toBe(returned);
37
+ });
38
+ });
package/src/sbType.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { SBType } from "storybook/internal/types";
2
+
3
+ /**
4
+ * Call a function `cb` for every type node in the storybook type tree.
5
+ * @param inputType the root type
6
+ * @param cb the function that is called for every type. If any non-nullish value is returned by `cb` the execution is stopped and this value is returned.
7
+ * @param parent optional, the parent type. Is only used as input for the `cb` function and provided when recursing.
8
+ * @returns the first non-nullish value that is returned by `cb`
9
+ */
10
+ export const walkTree = <TValue>(
11
+ inputType: SBType,
12
+ cb: (sb: SBType, parent?: SBType) => TValue,
13
+ parent?: SBType,
14
+ ): TValue | undefined => {
15
+ const shouldReturn = cb(inputType, parent);
16
+ if (shouldReturn) {
17
+ return shouldReturn;
18
+ }
19
+
20
+ if (inputType.name === "union" || inputType.name === "intersection") {
21
+ return inputType.value.reduce<TValue | undefined>(
22
+ (prev, it) => prev ?? walkTree(it, cb, inputType),
23
+ undefined,
24
+ );
25
+ }
26
+ if (inputType.name === "array") {
27
+ return walkTree(inputType.value, cb, inputType);
28
+ }
29
+ if (inputType.name === "object") {
30
+ return Object.values(inputType.value).reduce<TValue | undefined>(
31
+ (prev, it) => prev ?? walkTree(it, cb, inputType),
32
+ undefined,
33
+ );
34
+ }
35
+ };
package/src/theme.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { ONYX_BREAKPOINTS as RAW_ONYX_BREAKPOINTS, type OnyxBreakpoint } from "sit-onyx";
1
2
  import onyxVariables from "sit-onyx/themes/onyx.json";
2
- import { ONYX_BREAKPOINTS as RAW_ONYX_BREAKPOINTS, type OnyxBreakpoint } from "sit-onyx/types";
3
3
  import { create, type ThemeVars, type ThemeVarsPartial } from "storybook/internal/theming";
4
4
  import onyxLogo from "./assets/logo-onyx.svg";
5
5
 
package/src/types.ts CHANGED
@@ -1,52 +1,3 @@
1
- import type { ComponentPropsAndSlots, Meta } from "@storybook/vue3";
2
-
3
- /**
4
- * Extracts all event names defined by e.g. `defineEmits()` from the given Vue component.
5
- *
6
- * @example
7
- * ```ts
8
- * import Input from "./Input.vue";
9
- * type InputEvents = ExtractVueEventNames<typeof Input>; // e.g. "input" | "change"
10
- * ```
11
- */
12
- export type ExtractVueEventNames<VueComponent> =
13
- Extract<
14
- // extract all props/events of the vue component that are functions
15
- ExtractKeysByValueType<
16
- // this generic type will extract ALL props and events from the given Vue component
17
- ComponentPropsAndSlots<VueComponent>,
18
- // emits are declared as functions, so we only take props/events that are functions and ignore the rest
19
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- We must use any here to match the type defined by Vue
20
- ((...args: any) => any) | undefined
21
- >,
22
- // filter out potential function properties by just picking events that start with "on"
23
- `on${string}`
24
- > extends `on${infer EventName}`
25
- ? // until now the extracted event names still start with "on" but we want to have the plain event name
26
- // so we will remove the "on" prefix and uncapitalized the first letter so e.g. "onClick" becomes "click"
27
- Uncapitalize<EventName>
28
- : never;
29
-
30
- /**
31
- * Extracts only the keys from T whose value type satisfies U.
32
- *
33
- * @example
34
- * ```ts
35
- * type Test = ExtractKeysByValueType<{ a: boolean, b: number, c: boolean }, boolean>
36
- * // result: "a" | "c"
37
- * ```
38
- */
39
- export type ExtractKeysByValueType<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T] &
40
- keyof T;
41
-
42
- /**
43
- * Options for defining Storybook actions and v-models.
44
- */
45
- export type DefineStorybookActionsAndVModelsOptions<T> = Meta<T> & {
46
- component: NonNullable<T>;
47
- events: ExtractVueEventNames<T>[];
48
- };
49
-
50
1
  export type StorybookGlobalType<TValue> = {
51
2
  name: string;
52
3
  description: string;
@@ -55,5 +6,6 @@ export type StorybookGlobalType<TValue> = {
55
6
  icon: string;
56
7
  items: { value: TValue; right?: string; title: string }[];
57
8
  title?: string;
9
+ dynamicTitle?: boolean;
58
10
  };
59
11
  };
@@ -1,258 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
- //
3
- // This file is only a temporary copy of the improved source code generation for Storybook.
4
- // It is intended to be deleted once its officially released in Storybook itself, see:
5
- // https://github.com/storybookjs/storybook/pull/27194
6
- //
7
- import { expect, test } from "vitest";
8
- import { h } from "vue";
9
- import {
10
- generatePropsSourceCode,
11
- generateSlotSourceCode,
12
- generateSourceCode,
13
- getFunctionParamNames,
14
- parseDocgenInfo,
15
- type SourceCodeGeneratorContext,
16
- } from "./source-code-generator";
17
-
18
- test("should generate source code for props", () => {
19
- const ctx: SourceCodeGeneratorContext = {
20
- scriptVariables: {},
21
- imports: {},
22
- };
23
-
24
- const code = generatePropsSourceCode(
25
- {
26
- a: "foo",
27
- b: '"I am double quoted"',
28
- c: 42,
29
- d: true,
30
- e: false,
31
- f: [1, 2, 3],
32
- g: {
33
- g1: "foo",
34
- g2: 42,
35
- },
36
- h: undefined,
37
- i: null,
38
- j: "",
39
- k: BigInt(9007199254740991),
40
- l: Symbol(),
41
- m: Symbol("foo"),
42
- modelValue: "test-v-model",
43
- otherModelValue: 42,
44
- default: "default slot",
45
- testSlot: "test slot",
46
- },
47
- ["default", "testSlot"],
48
- ["update:modelValue", "update:otherModelValue"],
49
- ctx,
50
- );
51
-
52
- expect(code).toBe(
53
- `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"`,
54
- );
55
-
56
- expect(ctx.scriptVariables).toStrictEqual({
57
- f: `[1,2,3]`,
58
- g: `{"g1":"foo","g2":42}`,
59
- modelValue: 'ref("test-v-model")',
60
- otherModelValue: "ref(42)",
61
- });
62
-
63
- expect(Array.from(ctx.imports.vue.values())).toStrictEqual(["ref"]);
64
- });
65
-
66
- test("should generate source code for slots", () => {
67
- // slot code generator should support primitive values (string, number etc.)
68
- // but also VNodes (e.g. created using h()) so custom Vue components can also be used
69
- // inside slots with proper generated code
70
-
71
- const slots = {
72
- default: "default content",
73
- a: "a content",
74
- b: 42,
75
- c: true,
76
- // single VNode without props
77
- d: h("div", "d content"),
78
- // VNode with props and single child
79
- e: h("div", { style: "color:red" }, "e content"),
80
- // VNode with props and single child returned as getter
81
- f: h("div", { style: "color:red" }, () => "f content"),
82
- // VNode with multiple children
83
- g: h("div", { style: "color:red" }, [
84
- "child 1",
85
- h("span", { style: "color:green" }, "child 2"),
86
- ]),
87
- // VNode multiple children but returned as getter
88
- h: h("div", { style: "color:red" }, () => [
89
- "child 1",
90
- h("span", { style: "color:green" }, "child 2"),
91
- ]),
92
- // VNode with multiple and nested children
93
- i: h("div", { style: "color:red" }, [
94
- "child 1",
95
- h("span", { style: "color:green" }, ["nested child 1", h("p", "nested child 2")]),
96
- ]),
97
- j: ["child 1", "child 2"],
98
- k: null,
99
- l: { foo: "bar" },
100
- m: BigInt(9007199254740991),
101
- };
102
-
103
- const expectedCode = `default content
104
-
105
- <template #a>a content</template>
106
-
107
- <template #b>42</template>
108
-
109
- <template #c>true</template>
110
-
111
- <template #d><div>d content</div></template>
112
-
113
- <template #e><div style="color:red">e content</div></template>
114
-
115
- <template #f><div style="color:red">f content</div></template>
116
-
117
- <template #g><div style="color:red">child 1
118
- <span style="color:green">child 2</span></div></template>
119
-
120
- <template #h><div style="color:red">child 1
121
- <span style="color:green">child 2</span></div></template>
122
-
123
- <template #i><div style="color:red">child 1
124
- <span style="color:green">nested child 1
125
- <p>nested child 2</p></span></div></template>
126
-
127
- <template #j>child 1
128
- child 2</template>
129
-
130
- <template #l>{"foo":"bar"}</template>
131
-
132
- <template #m>{{ BigInt(9007199254740991) }}</template>`;
133
-
134
- let actualCode = generateSlotSourceCode(slots, Object.keys(slots), {
135
- scriptVariables: {},
136
- imports: {},
137
- });
138
- expect(actualCode).toBe(expectedCode);
139
-
140
- // should generate the same code if getters/functions are used to return the slot content
141
- const slotsWithGetters = Object.entries(slots).reduce<
142
- Record<string, () => (typeof slots)[keyof typeof slots]>
143
- >((obj, [slotName, value]) => {
144
- obj[slotName] = () => value;
145
- return obj;
146
- }, {});
147
-
148
- actualCode = generateSlotSourceCode(slotsWithGetters, Object.keys(slotsWithGetters), {
149
- scriptVariables: {},
150
- imports: {},
151
- });
152
- expect(actualCode).toBe(expectedCode);
153
- });
154
-
155
- test("should generate source code for slots with bindings", () => {
156
- type TestBindings = {
157
- foo: string;
158
- bar?: number;
159
- };
160
-
161
- const slots = {
162
- a: ({ foo, bar }: TestBindings) => `Slot with bindings ${foo} and ${bar}`,
163
- b: ({ foo }: TestBindings) => h("a", { href: foo, target: foo }, `Test link: ${foo}`),
164
- };
165
-
166
- const expectedCode = `<template #a="{ foo, bar }">Slot with bindings {{ foo }} and {{ bar }}</template>
167
-
168
- <template #b="{ foo }"><a :href="foo" :target="foo">Test link: {{ foo }}</a></template>`;
169
-
170
- const actualCode = generateSlotSourceCode(slots, Object.keys(slots), {
171
- imports: {},
172
- scriptVariables: {},
173
- });
174
- expect(actualCode).toBe(expectedCode);
175
- });
176
-
177
- test("should generate source code with <script setup> block", () => {
178
- const actualCode = generateSourceCode({
179
- title: "MyComponent",
180
- component: {
181
- __docgenInfo: {
182
- slots: [{ name: "mySlot" }],
183
- events: [{ name: "update:c" }],
184
- },
185
- },
186
- args: {
187
- a: 42,
188
- b: "foo",
189
- c: [1, 2, 3],
190
- d: { bar: "baz" },
191
- mySlot: () => h("div", { test: [1, 2], d: { nestedProp: "foo" } }),
192
- },
193
- });
194
-
195
- expect(actualCode).toBe(`<script lang="ts" setup>
196
- import { ref } from "vue";
197
-
198
- const c = ref([1,2,3]);
199
-
200
- const d = {"bar":"baz"};
201
-
202
- const d1 = {"nestedProp":"foo"};
203
-
204
- const test = [1,2];
205
- </script>
206
-
207
- <template>
208
- <MyComponent :a="42" b="foo" v-model:c="c" :d="d"> <template #mySlot><div :d="d1" :test="test" /></template> </MyComponent>
209
- </template>`);
210
- });
211
-
212
- test.each([
213
- { __docgenInfo: "invalid-value", slotNames: [] },
214
- { __docgenInfo: {}, slotNames: [] },
215
- { __docgenInfo: { slots: "invalid-value" }, slotNames: [] },
216
- { __docgenInfo: { slots: ["invalid-value"] }, slotNames: [] },
217
- {
218
- __docgenInfo: { slots: [{ name: "slot-1" }, { name: "slot-2" }, { notName: "slot-3" }] },
219
- slotNames: ["slot-1", "slot-2"],
220
- },
221
- ])("should parse slots names from __docgenInfo", ({ __docgenInfo, slotNames }) => {
222
- const docgenInfo = parseDocgenInfo({ __docgenInfo });
223
- expect(docgenInfo.slotNames).toStrictEqual(slotNames);
224
- });
225
-
226
- test.each([
227
- { __docgenInfo: "invalid-value", eventNames: [] },
228
- { __docgenInfo: {}, eventNames: [] },
229
- { __docgenInfo: { events: "invalid-value" }, eventNames: [] },
230
- { __docgenInfo: { events: ["invalid-value"] }, eventNames: [] },
231
- {
232
- __docgenInfo: { events: [{ name: "event-1" }, { name: "event-2" }, { notName: "event-3" }] },
233
- eventNames: ["event-1", "event-2"],
234
- },
235
- ])("should parse event names from __docgenInfo", ({ __docgenInfo, eventNames }) => {
236
- const docgenInfo = parseDocgenInfo({ __docgenInfo });
237
- expect(docgenInfo.eventNames).toStrictEqual(eventNames);
238
- });
239
-
240
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
241
- test.each<{ fn: (...args: any[]) => unknown; expectedNames: string[] }>([
242
- { fn: () => ({}), expectedNames: [] },
243
- { fn: (a) => ({}), expectedNames: ["a"] },
244
- { fn: (a, b) => ({}), expectedNames: ["a", "b"] },
245
- { fn: (a, b, { c }) => ({}), expectedNames: ["a", "b", "{", "c", "}"] },
246
- { fn: ({ a, b }) => ({}), expectedNames: ["{", "a", "b", "}"] },
247
- {
248
- fn: {
249
- // simulate minified function after running "storybook build"
250
- toString: () => "({a:foo,b:bar})=>({})",
251
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
252
- } as (...args: any[]) => unknown,
253
- expectedNames: ["{", "a", "b", "}"],
254
- },
255
- ])("should extract function parameter names", ({ fn, expectedNames }) => {
256
- const paramNames = getFunctionParamNames(fn);
257
- expect(paramNames).toStrictEqual(expectedNames);
258
- });