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

Vue: Add Vue 3 support #13775

Merged
merged 10 commits into from
Feb 2, 2021
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
1 change: 1 addition & 0 deletions addons/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"terser-webpack-plugin": "^3.1.0",
"tmp": "^0.2.1",
"tslib": "^2.1.0",
"vue": "^2.6.10",
"web-component-analyzer": "^1.1.6",
"webpack": "^4.46.0",
"zone.js": "^0.11.3"
Expand Down
41 changes: 41 additions & 0 deletions app/vue3/README.md
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.
Copy link
Contributor Author

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?

Copy link
Member

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.

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 */ });
```
4 changes: 4 additions & 0 deletions app/vue3/bin/build.js
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');
3 changes: 3 additions & 0 deletions app/vue3/bin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env node

require('../dist/cjs/server');
80 changes: 80 additions & 0 deletions app/vue3/package.json
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"
}
17 changes: 17 additions & 0 deletions app/vue3/src/client/index.ts
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();
}
4 changes: 4 additions & 0 deletions app/vue3/src/client/preview/globals.ts
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
126 changes: 126 additions & 0 deletions app/vue3/src/client/preview/index.ts
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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be tested. I tried using the extends syntax to support object and function components.

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered making this a setup method

});
}

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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
59 changes: 59 additions & 0 deletions app/vue3/src/client/preview/render.ts
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>({});
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 reactive, right?


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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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');
}
}
23 changes: 23 additions & 0 deletions app/vue3/src/client/preview/types-6-0.ts
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>;
20 changes: 20 additions & 0 deletions app/vue3/src/client/preview/types.ts
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[];
}
4 changes: 4 additions & 0 deletions app/vue3/src/server/build.ts
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);
Loading