@sit-onyx/storybook-utils 1.0.0-beta.7 → 1.0.0-beta.70
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/package.json +9 -6
- package/src/actions.spec.ts +29 -0
- package/src/actions.ts +70 -58
- package/src/events.ts +777 -0
- package/src/index.css +11 -0
- package/src/index.ts +1 -0
- package/src/preview.spec.ts +3 -9
- package/src/preview.ts +19 -9
- package/src/sbType.spec.ts +38 -0
- package/src/sbType.ts +104 -0
- package/src/theme.ts +1 -1
- package/src/types.ts +1 -49
- package/src/source-code-generator.spec.ts +0 -258
- package/src/source-code-generator.ts +0 -539
package/src/index.css
CHANGED
|
@@ -24,3 +24,14 @@
|
|
|
24
24
|
/* same as Storybook color "textMuted" inside ./theme.ts */
|
|
25
25
|
color: var(--onyx-color-text-icons-neutral-medium);
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/* To prevent bg flashing when changing between elements in darkmode */
|
|
29
|
+
.sb-preparing-story,
|
|
30
|
+
.sb-preparing-docs {
|
|
31
|
+
background-color: transparent;
|
|
32
|
+
}
|
|
33
|
+
/* removed placeholder for the same reason */
|
|
34
|
+
.sb-previewBlock,
|
|
35
|
+
.sb-argstableBlock {
|
|
36
|
+
display: none;
|
|
37
|
+
}
|
package/src/index.ts
CHANGED
package/src/preview.spec.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
11
|
-
const
|
|
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 {
|
|
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(
|
|
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,104 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ArgTypesEnhancer,
|
|
3
|
+
InputType,
|
|
4
|
+
SBType,
|
|
5
|
+
StrictInputType,
|
|
6
|
+
} from "storybook/internal/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Call a function `cb` for every type node in the storybook type tree.
|
|
10
|
+
* @param inputType the root type
|
|
11
|
+
* @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.
|
|
12
|
+
* @param parent optional, the parent type. Is only used as input for the `cb` function and provided when recursing.
|
|
13
|
+
* @returns the first non-nullish value that is returned by `cb`
|
|
14
|
+
*/
|
|
15
|
+
export const walkTree = <TValue>(
|
|
16
|
+
inputType: SBType,
|
|
17
|
+
cb: (sb: SBType, parent?: SBType) => TValue,
|
|
18
|
+
parent?: SBType,
|
|
19
|
+
): TValue | undefined => {
|
|
20
|
+
const shouldReturn = cb(inputType, parent);
|
|
21
|
+
if (shouldReturn) {
|
|
22
|
+
return shouldReturn;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (inputType.name === "union" || inputType.name === "intersection") {
|
|
26
|
+
return inputType.value.reduce<TValue | undefined>(
|
|
27
|
+
(prev, it) => prev ?? walkTree(it, cb, inputType),
|
|
28
|
+
undefined,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (inputType.name === "array") {
|
|
32
|
+
return walkTree(inputType.value, cb, inputType);
|
|
33
|
+
}
|
|
34
|
+
if (inputType.name === "object") {
|
|
35
|
+
return Object.values(inputType.value).reduce<TValue | undefined>(
|
|
36
|
+
(prev, it) => prev ?? walkTree(it, cb, inputType),
|
|
37
|
+
undefined,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const SB_TYPE_CONTROL_MAP: Partial<Record<SBType["name"], InputType["control"]>> = {
|
|
43
|
+
boolean: { type: "boolean" },
|
|
44
|
+
string: { type: "text" },
|
|
45
|
+
number: { type: "number" },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const getFormInjectedParent = (symbol: string, inputType?: StrictInputType) => {
|
|
49
|
+
if (!inputType?.type || inputType.table?.defaultValue?.summary !== symbol) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return walkTree(inputType.type, (elem, parent) =>
|
|
54
|
+
elem.name === "symbol" || (elem.name === "other" && elem.value === "unique symbol")
|
|
55
|
+
? parent
|
|
56
|
+
: undefined,
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Can be used to create an `ArgTypesEnhancer` which matches a Symbol that is used as default Prop.
|
|
62
|
+
* When it matches the passed description text will be set.
|
|
63
|
+
*
|
|
64
|
+
* @param symbol description of the symbol that should be matched.
|
|
65
|
+
* @param description the description text that should be shown in Storybook for this prop.
|
|
66
|
+
* @returns An `ArgTypesEnhancer` which can be passed to storybook.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { createSymbolArgTypeEnhancer } from "@sit-onyx/storybook-utils";
|
|
71
|
+
*
|
|
72
|
+
* export const enhanceFormInjectedSymbol = createSymbolArgTypeEnhancer(
|
|
73
|
+
* "FORM_INJECTED_SYMBOL",
|
|
74
|
+
* "If no value (or `undefined`) is provided, `FORM_INJECTED_SYMBOL` is the internal default value for this prop.\n" +
|
|
75
|
+
* "In that case the props value will be derived from it's parent form (if it exists).\n",
|
|
76
|
+
* );
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export const createSymbolArgTypeEnhancer = (
|
|
80
|
+
symbol: string,
|
|
81
|
+
description: string,
|
|
82
|
+
): ArgTypesEnhancer => {
|
|
83
|
+
return (context) => {
|
|
84
|
+
Object.values(context.argTypes)
|
|
85
|
+
.map((argType) => {
|
|
86
|
+
const parent = getFormInjectedParent(symbol, argType);
|
|
87
|
+
return { argType, parent };
|
|
88
|
+
})
|
|
89
|
+
.filter(({ parent }) => parent)
|
|
90
|
+
.forEach(({ argType, parent }) => {
|
|
91
|
+
const firstAvailableControl = walkTree(
|
|
92
|
+
parent || argType.type!,
|
|
93
|
+
(sb) => SB_TYPE_CONTROL_MAP[sb.name],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (firstAvailableControl && argType.table?.defaultValue) {
|
|
97
|
+
argType.control = firstAvailableControl;
|
|
98
|
+
argType.table.defaultValue.detail = description;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return context.argTypes;
|
|
103
|
+
};
|
|
104
|
+
};
|
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
|
-
});
|