-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
Vue: Add Vue 3 support #13775
Vue: Add Vue 3 support #13775
Changes from all commits
4be1598
c42981c
1761302
9c35e55
429c3cc
fafc6fc
6237d92
2bdb527
4024692
6d31d1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# Storybook for Vue 3 | ||
|
||
Storybook for Vue 3 is a UI development environment for your Vue 3 components. | ||
With it, you can visualize different states of your UI components and develop them interactively. | ||
|
||
![Storybook Screenshot](https://github.com/storybookjs/storybook/blob/master/media/storybook-intro.gif) | ||
|
||
Storybook runs outside of your app. | ||
So you can develop UI components in isolation without worrying about app specific dependencies and requirements. | ||
|
||
## Getting Started | ||
|
||
```sh | ||
cd my-vue3-app | ||
npx -p @storybook/cli sb init | ||
``` | ||
|
||
For more information visit: [storybook.js.org](https://storybook.js.org) | ||
|
||
--- | ||
|
||
Storybook also comes with a lot of [addons](https://storybook.js.org/docs/vue3/configure/storybook-addons) and a great API to customize as you wish. | ||
You can also build a [static version](https://storybook.js.org/docs/vue3/workflows/publish-storybook) of your storybook and deploy it anywhere you want. | ||
|
||
## Extending the Vue application | ||
|
||
Storybook creates a [Vue 3 application](https://v3.vuejs.org/api/application-api.html#application-api) for your component preview, which can be imported as `import { app } from '@storybook/vue3'`. | ||
|
||
When using global custom components (`app.component`), directives (`app.directive`), extensions (`app.use`), or other application methods, you will need to configure those in the `./storybook/preview.js` file. | ||
|
||
For example: | ||
|
||
```js | ||
// .storybook/preview.js | ||
|
||
import { app } from '@storybook/vue3'; | ||
|
||
app.use(MyPlugin); | ||
app.component('my-component', MyComponent); | ||
app.mixin({ /* My mixin */ }); | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
#!/usr/bin/env node | ||
|
||
process.env.NODE_ENV = process.env.NODE_ENV || 'production'; | ||
require('../dist/cjs/server/build'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
#!/usr/bin/env node | ||
|
||
require('../dist/cjs/server'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
{ | ||
"name": "@storybook/vue3", | ||
"version": "6.2.0-alpha.19", | ||
"description": "Storybook for Vue 3: Develop Vue 3 Components in isolation with Hot Reloading.", | ||
"keywords": [ | ||
"storybook" | ||
], | ||
"homepage": "https://github.com/storybookjs/storybook/tree/master/app/vue3", | ||
"bugs": { | ||
"url": "https://github.com/storybookjs/storybook/issues" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/storybookjs/storybook.git", | ||
"directory": "app/vue3" | ||
}, | ||
"license": "MIT", | ||
"main": "dist/cjs/client/index.js", | ||
"module": "dist/esm/client/index.js", | ||
"types": "dist/ts3.9/client/index.d.ts", | ||
"typesVersions": { | ||
"<3.8": { | ||
"*": [ | ||
"dist/ts3.4/*" | ||
] | ||
} | ||
}, | ||
"bin": { | ||
"build-storybook": "./bin/build.js", | ||
"start-storybook": "./bin/index.js", | ||
"storybook-server": "./bin/index.js" | ||
}, | ||
"files": [ | ||
"bin/**/*", | ||
"dist/**/*", | ||
"README.md", | ||
"*.js", | ||
"*.d.ts" | ||
], | ||
"scripts": { | ||
"prepare": "node ../../scripts/prepare.js" | ||
}, | ||
"dependencies": { | ||
"@storybook/addons": "6.2.0-alpha.19", | ||
"@storybook/core": "6.2.0-alpha.19", | ||
"@types/webpack-env": "^1.16.0", | ||
"core-js": "^3.8.2", | ||
"global": "^4.4.0", | ||
"react": "16.14.0", | ||
"react-dom": "16.14.0", | ||
"read-pkg-up": "^7.0.1", | ||
"regenerator-runtime": "^0.13.7", | ||
"ts-dedent": "^2.0.0", | ||
"ts-loader": "^6.2.2", | ||
"vue-docgen-api": "^4.34.2", | ||
"vue-docgen-loader": "^1.5.0", | ||
"webpack": "^4.46.0" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^14.14.20", | ||
"@types/webpack": "^4.41.26", | ||
"@vue/compiler-sfc": "^3.0.0", | ||
"vue": "^3.0.0", | ||
"vue-loader": "^16.0.0" | ||
}, | ||
"peerDependencies": { | ||
"@babel/core": "*", | ||
"@vue/compiler-sfc": "^3.0.0", | ||
"babel-loader": "^7.0.0 || ^8.0.0", | ||
"vue": "^3.0.0", | ||
"vue-loader": "^16.0.0" | ||
}, | ||
"engines": { | ||
"node": ">=10.13.0" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"gitHead": "ed19e4b88b0fbc36d10379149b7c98194254897e" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
export { | ||
storiesOf, | ||
setAddon, | ||
addDecorator, | ||
addParameters, | ||
configure, | ||
getStorybook, | ||
forceReRender, | ||
raw, | ||
app, | ||
} from './preview'; | ||
|
||
export * from './preview/types-6-0'; | ||
|
||
if (module && module.hot && module.hot.decline) { | ||
module.hot.decline(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { window } from 'global'; | ||
|
||
window.STORYBOOK_REACT_CLASSES = {}; | ||
window.STORYBOOK_ENV = 'vue3'; | ||
phated marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { ConcreteComponent, Component, App, defineComponent, h } from 'vue'; | ||
import { start } from '@storybook/core/client'; | ||
import { | ||
ClientStoryApi, | ||
StoryFn, | ||
DecoratorFunction, | ||
StoryContext, | ||
Loadable, | ||
} from '@storybook/addons'; | ||
|
||
import './globals'; | ||
import { IStorybookSection, StoryFnVueReturnType } from './types'; | ||
|
||
import render, { storybookApp } from './render'; | ||
|
||
const PROPS = 'STORYBOOK_PROPS'; | ||
|
||
function prepare(story: StoryFnVueReturnType, innerStory?: ConcreteComponent): Component | null { | ||
if (story == null) { | ||
return null; | ||
} | ||
|
||
if (innerStory) { | ||
return { | ||
extends: story, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to be tested. I tried using the |
||
components: { story: innerStory }, | ||
props: innerStory.props, | ||
inject: { | ||
props: { | ||
from: PROPS, | ||
default: null, | ||
}, | ||
}, | ||
provide() { | ||
return { | ||
[PROPS]: this.props || this.$props, | ||
}; | ||
}, | ||
}; | ||
} | ||
|
||
return defineComponent({ | ||
props: story.props, | ||
inject: { | ||
props: { | ||
from: PROPS, | ||
default: null, | ||
}, | ||
}, | ||
render() { | ||
return h(story, this.props || this.$props); | ||
}, | ||
Comment on lines
+50
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered making this a |
||
}); | ||
} | ||
|
||
const defaultContext: StoryContext = { | ||
id: 'unspecified', | ||
name: 'unspecified', | ||
kind: 'unspecified', | ||
parameters: {}, | ||
args: {}, | ||
argTypes: {}, | ||
globals: {}, | ||
}; | ||
|
||
function decorateStory( | ||
storyFn: StoryFn<StoryFnVueReturnType>, | ||
decorators: DecoratorFunction<ConcreteComponent>[] | ||
): StoryFn<Component> { | ||
return decorators.reduce( | ||
(decorated: StoryFn<ConcreteComponent>, decorator) => ( | ||
context: StoryContext = defaultContext | ||
) => { | ||
let story; | ||
|
||
const decoratedStory = decorator( | ||
({ parameters, ...innerContext }: StoryContext = {} as StoryContext) => { | ||
story = decorated({ ...context, ...innerContext }); | ||
return story; | ||
}, | ||
context | ||
); | ||
|
||
if (!story) { | ||
story = decorated(context); | ||
} | ||
|
||
if (decoratedStory === story) { | ||
return story; | ||
} | ||
|
||
return prepare(decoratedStory, story); | ||
}, | ||
(context) => prepare(storyFn(context)) | ||
); | ||
} | ||
const framework = 'vue3'; | ||
|
||
interface ClientApi extends ClientStoryApi<StoryFnVueReturnType> { | ||
setAddon(addon: any): void; | ||
configure(loader: Loadable, module: NodeModule): void; | ||
getStorybook(): IStorybookSection[]; | ||
clearDecorators(): void; | ||
forceReRender(): void; | ||
raw: () => any; // todo add type | ||
load: (...args: any[]) => void; | ||
app: App; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct place to put this type? |
||
} | ||
|
||
const api = start(render, { decorateStory }); | ||
|
||
export const storiesOf: ClientApi['storiesOf'] = (kind, m) => { | ||
return (api.clientApi.storiesOf(kind, m) as ReturnType<ClientApi['storiesOf']>).addParameters({ | ||
framework, | ||
}); | ||
}; | ||
|
||
export const configure: ClientApi['configure'] = (...args) => api.configure(framework, ...args); | ||
export const { addDecorator } = api.clientApi; | ||
export const { addParameters } = api.clientApi; | ||
export const { clearDecorators } = api.clientApi; | ||
export const { setAddon } = api.clientApi; | ||
export const { forceReRender } = api; | ||
export const { getStorybook } = api.clientApi; | ||
export const { raw } = api.clientApi; | ||
export const app: ClientApi['app'] = storybookApp; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import type { Args } from '@storybook/addons'; | ||
import dedent from 'ts-dedent'; | ||
import { createApp, h, shallowRef, ComponentPublicInstance } from 'vue'; | ||
import { RenderContext, StoryFnVueReturnType } from './types'; | ||
|
||
const activeStoryComponent = shallowRef<StoryFnVueReturnType | null>(null); | ||
const activeProps = shallowRef<Args>({}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is a shallow ref fine here? I don't think these props need to be wrapped in a |
||
|
||
let root: ComponentPublicInstance | null = null; | ||
|
||
export const storybookApp = createApp({ | ||
// If an end-user calls `unmount` on the app, we need to clear our root variable | ||
unmounted() { | ||
root = null; | ||
}, | ||
Comment on lines
+12
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to make sure this works and can be re-mounted. |
||
|
||
setup() { | ||
return () => { | ||
if (!activeStoryComponent.value) | ||
throw new Error('No Vue 3 Story available. Was it set correctly?'); | ||
return h(activeStoryComponent.value, activeProps.value); | ||
}; | ||
}, | ||
}); | ||
|
||
export default function render({ | ||
storyFn, | ||
kind, | ||
name, | ||
args, | ||
showMain, | ||
showError, | ||
showException, | ||
forceRender, | ||
}: RenderContext) { | ||
storybookApp.config.errorHandler = showException; | ||
|
||
const element: StoryFnVueReturnType = storyFn(); | ||
|
||
if (!element) { | ||
showError({ | ||
title: `Expecting a Vue component from the story: "${name}" of "${kind}".`, | ||
description: dedent` | ||
Did you forget to return the Vue component from the story? | ||
Use "() => ({ template: '<my-comp></my-comp>' })" or "() => ({ components: MyComp, template: '<my-comp></my-comp>' })" when defining the story. | ||
`, | ||
}); | ||
return; | ||
} | ||
|
||
showMain(); | ||
|
||
activeStoryComponent.value = element; | ||
activeProps.value = args; | ||
|
||
if (!root) { | ||
root = storybookApp.mount('#root'); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { ConcreteComponent } from 'vue'; | ||
import { Args as DefaultArgs, Annotations, BaseMeta, BaseStory } from '@storybook/addons'; | ||
import { StoryFnVueReturnType } from './types'; | ||
|
||
export type { Args, ArgTypes, Parameters, StoryContext } from '@storybook/addons'; | ||
|
||
type VueComponent = ConcreteComponent; | ||
type VueReturnType = StoryFnVueReturnType; | ||
|
||
/** | ||
* Metadata to configure the stories for a component. | ||
* | ||
* @see [Default export](https://storybook.js.org/docs/formats/component-story-format/#default-export) | ||
*/ | ||
export type Meta<Args = DefaultArgs> = BaseMeta<VueComponent> & Annotations<Args, VueReturnType>; | ||
|
||
/** | ||
* Story function that represents a component example. | ||
* | ||
* @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports) | ||
*/ | ||
export type Story<Args = DefaultArgs> = BaseStory<Args, VueReturnType> & | ||
Annotations<Args, VueReturnType>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { ConcreteComponent } from 'vue'; | ||
|
||
export { RenderContext } from '@storybook/core'; | ||
|
||
export interface ShowErrorArgs { | ||
title: string; | ||
description: string; | ||
} | ||
|
||
export type StoryFnVueReturnType = ConcreteComponent; | ||
|
||
export interface IStorybookStory { | ||
name: string; | ||
render: () => any; | ||
} | ||
|
||
export interface IStorybookSection { | ||
kind: string; | ||
stories: IStorybookStory[]; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { buildStatic } from '@storybook/core/server'; | ||
import options from './options'; | ||
|
||
buildStatic(options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does
vue3
section get generated on the docs website?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be added in the frontpage repo. @jonniebigodes can help with that.