├── .codeclimate.yml
├── .editorconfig
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .nvmrc
├── .storybook
├── config.js
└── webpack.config.js
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
└── logo.png
├── example
├── .babelrc
├── .gitignore
├── .storybook
│ ├── addons.js
│ ├── config.js
│ └── webpack.config.js
├── components
│ ├── BaseBlank.vue
│ ├── BaseButton.vue
│ ├── NumberList.vue
│ └── customDocs
│ │ └── wrapper
│ │ ├── Wrapper.css
│ │ └── Wrapper.vue
├── package.json
└── stories
│ ├── compatibilities
│ └── official
│ │ ├── a11y.stories.js
│ │ ├── backgrounds.stories.js
│ │ ├── components
│ │ └── MyButton.vue
│ │ └── knobs.stories.js
│ ├── examples
│ └── index.stories.js
│ └── issues
│ ├── 21
│ ├── BulmaButton.vue
│ └── index.stories.js
│ ├── 50
│ └── index.stories.js
│ ├── 53
│ └── index.stories.js
│ ├── 59
│ ├── child.vue
│ ├── index.stories.js
│ └── parent.vue
│ ├── 63
│ ├── component.vue
│ └── index.stories.js
│ ├── 71
│ ├── component.vue
│ └── index.stories.js
│ ├── 72
│ ├── component.vue
│ └── index.stories.js
│ ├── 85
│ ├── component.vue
│ ├── index.stories.js
│ └── instance.js
│ ├── 86
│ ├── component.vue
│ ├── index.stories.js
│ └── mixin.vue
│ ├── 95
│ ├── component.vue
│ └── index.stories.js
│ ├── 119
│ ├── component.js
│ └── index.stories.js
│ ├── 122
│ ├── camel.vue
│ ├── index.stories.js
│ ├── kebab.vue
│ └── mixed.vue
│ └── 126
│ ├── component.vue
│ └── index.stories.js
├── loader
└── index.js
├── netlify.toml
├── package.json
├── rollup.config.js
├── src
├── addon.ts
├── components
│ ├── Component
│ │ ├── index.vue
│ │ └── style.css
│ ├── Docs
│ │ ├── index.vue
│ │ └── style.css
│ ├── Header
│ │ ├── index.vue
│ │ └── style.css
│ ├── Preview
│ │ ├── index.vue
│ │ └── style.css
│ ├── SectionContainer
│ │ ├── index.stories.js
│ │ ├── index.vue
│ │ └── style.css
│ ├── Separator
│ │ ├── index.vue
│ │ └── style.css
│ ├── StorySource
│ │ ├── index.vue
│ │ └── style.css
│ ├── Summary
│ │ ├── index.vue
│ │ └── style.css
│ ├── Table
│ │ ├── index.stories.js
│ │ ├── index.vue
│ │ └── style.css
│ ├── Wrapper
│ │ ├── index.stories.js
│ │ └── index.vue
│ └── index.ts
├── extract
│ ├── decideTargets.ts
│ ├── extractDocgenInfo.ts
│ ├── getProps.ts
│ ├── index.ts
│ └── lookup.ts
├── index.ts
├── lib.d.ts
├── options.ts
├── register.tsx
├── types
│ ├── dedent-tabs.ts
│ ├── info.ts
│ └── vue.ts
├── typings.d.ts
├── utils
│ ├── constructorToString.spec.ts
│ ├── constructorToString.ts
│ ├── getJSXFromRenderFn.spec.ts
│ ├── getJSXFromRenderFn.ts
│ ├── getTagNames.spec.ts
│ ├── getTagNames.ts
│ ├── removeDuplicates.spec.ts
│ └── removeDuplicates.ts
└── view
│ └── index.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | plugins:
4 | duplication:
5 | enabled: true
6 | config:
7 | count_threshold: 3
8 |
9 | exclude_patterns:
10 | - 'example/'
11 | - '.storybook/'
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset = utf-8
3 | end_of_line = lf
4 | insert_final_newline = true
5 | trim_trailing_whitespace = true
6 | indent_size = 2
7 | indent_style = space
8 |
9 | [*.md]
10 | trim_trailing_whitespace = false
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | ---
5 |
6 | **Describe the bug**
7 | A clear and concise description of what the bug is.
8 |
9 | **To Reproduce**
10 | Steps to reproduce the behavior:
11 |
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Versions**
24 |
25 | - `storybook-addon-vue-info`:
26 | - `@storybook/vue`:
27 |
28 | **Additional context**
29 | Add any other context about the problem here.
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Do not add your OS/IDE/Editor files to here.
2 | # Please use global ignore instead.
3 | # https://help.github.com/articles/ignoring-files/#create-a-global-gitignore
4 |
5 | node_modules
6 | *.log
7 |
8 | # Rollup cache
9 | .rpt2_cache
10 |
11 | # Package packed by `npm pack`
12 | *.tgz
13 |
14 | lib
15 |
16 | example/build
17 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v11.6.0
2 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/vue'
2 |
3 | const req = require.context('../src/components', true, /.stories.js$/)
4 |
5 | configure(function loadStories() {
6 | req.keys().forEach(f => req(f))
7 | }, module)
8 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (baseConfig, env, defaultConfig) => {
2 | defaultConfig.module.rules = defaultConfig.module.rules.filter(
3 | r => r.test.toString() !== /\.css$/.toString()
4 | )
5 |
6 | defaultConfig.module.rules.push({
7 | test: /\.css$/,
8 | oneOf: [
9 | {
10 | resourceQuery: /module/,
11 | use: [
12 | 'vue-style-loader',
13 | {
14 | loader: 'css-loader',
15 | options: {
16 | modules: true
17 | }
18 | }
19 | ]
20 | },
21 | {
22 | use: ['vue-style-loader', 'css-loader']
23 | }
24 | ]
25 | })
26 |
27 | return defaultConfig
28 | }
29 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | cache: yarn
4 |
5 | script:
6 | - yarn lint
7 | - yarn test
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ## [1.4.2] - 2020-02-24
11 |
12 | ### Fixed
13 |
14 | - Stringify function props' default value. (Issue: [#126](https://github.com/pocka/storybook-addon-vue-info/issues/126), PR: [#127](https://github.com/pocka/storybook-addon-vue-info/pull/127))
15 |
16 | ## [1.4.1] - 2019-12-19
17 |
18 | ### Fixed
19 |
20 | - Fix incorrect component detection.
21 |
22 | ## [1.4.0] - 2019-12-11
23 |
24 | ### Added
25 |
26 | - Add `casing` option. (Issue: [#122](https://github.com/pocka/storybook-addon-vue-info/issues/122), PR: [#123](https://github.com/pocka/storybook-addon-vue-info/pull/123))
27 |
28 | ## [1.3.4] - 2019-11-15
29 |
30 | ### Fixed
31 |
32 | - Fix compatibility issue with vue-docgen-api@4.
33 |
34 | ## [1.3.3] - 2019-11-14
35 |
36 | ### Deprecated
37 |
38 | - Deprecate the custom loader in favor of vue-docgen-loader. (PR: [#121](https://github.com/pocka/storybook-addon-vue-info/pull/121))
39 |
40 | ## [1.3.2] - 2019-09-24
41 |
42 | ### Fixed
43 |
44 | - Do not reset preview style with `all: initial`. (Issue: [#21](https://github.com/pocka/storybook-addon-vue-info/issues/21), PR: [#115](https://github.com/pocka/storybook-addon-vue-info/pull/115))
45 |
46 | ## [1.3.1] - 2019-09-07
47 |
48 | ### Fixed
49 |
50 | - Ignore story decorators. (Issue: [#50](https://github.com/pocka/storybook-addon-vue-info/issues/50), PR: [#113](https://github.com/pocka/storybook-addon-vue-info/pull/113))
51 |
52 | ## [1.3.0] - 2019-08-26
53 |
54 | ### Added
55 |
56 | - Add `description` story option, which supports descriptions for props, events and slots.
57 |
58 | ### Deprecated
59 |
60 | - Deprecate `propsDescription` in favor of `description`.
61 |
62 | ## [1.2.3] - 2019-08-15
63 |
64 | ### Security
65 |
66 | - Bump marked from 0.6.2 to 0.7.0. (Issue: [#110](https://github.com/pocka/storybook-addon-vue-info/issues/110), Vulnerability detail: [npm](https://github.com/pocka/storybook-addon-vue-info/issues/110))
67 |
68 | ## [1.2.2] - 2019-07-26
69 |
70 | ### Fixed
71 |
72 | - Get default value only if prop is specified as an object. (Issue: [#107](https://github.com/pocka/storybook-addon-vue-info/issues/107), PR: [#108](https://github.com/pocka/storybook-addon-vue-info/pull/108))
73 |
74 | ## [1.2.1] - 2019-06-06
75 |
76 | ### Fixed
77 |
78 | - Styling problems in props table on narrow screen/addon-panel (PR: [#103](https://github.com/pocka/storybook-addon-vue-info/pull/103))
79 |
80 | ## [1.2.0] - 2019-05-11
81 |
82 | ### Added
83 |
84 | - `previewClassName` and `previewStyle` option (Issue: [#93](https://github.com/pocka/storybook-addon-vue-info/issues/93), PR: [#98](https://github.com/pocka/storybook-addon-vue-info/pull/98))
85 |
86 | ## [1.1.2] - 2019-05-11
87 |
88 | ### Fixed
89 |
90 | - Show `any` for prop type when the prop has no `type` field (Issue: [#95](https://github.com/pocka/storybook-addon-vue-info/issues/95), PR: [#97](https://github.com/pocka/storybook-addon-vue-info/pull/97))
91 |
92 | ## [1.1.1] - 2019-04-20
93 |
94 | ### Fixed
95 |
96 | - Use dedent-tabs, which is a fork of dedent, to support tab dedenting. (Issue: [#79](https://github.com/pocka/storybook-addon-vue-info/issues/79), PR: [#90](https://github.com/pocka/storybook-addon-vue-info/pull/90))
97 |
98 | ## [1.1.0] - 2019-04-20
99 |
100 | ### Added
101 |
102 | - Allow passing options for vue-docgen-api (PR: [#86](https://github.com/pocka/storybook-addon-vue-info/pull/86)).
103 | - Show error message if vue-docgen-api fails to parse component (PR: [#85](https://github.com/pocka/storybook-addon-vue-info/pull/85)).
104 |
105 | ### Fixed
106 |
107 | - Remove global affecting highlight.js' CSS (PR: [#87](https://github.com/pocka/storybook-addon-vue-info/pull/87)).
108 |
109 | ## [1.0.1] - 2019-03-21
110 |
111 | ### Fixed
112 |
113 | - Fix re-render issue (only when using `docsInPanel = true`).
114 |
115 | ## [1.0.0] - 2019-03-06
116 |
117 | ### Changed
118 |
119 | - **BREAKING**: Adapt to the latest decorator API. You need to specify the info parameters for each story (can omit by setting default).
120 | - **BREAKING**: `propTables` option changed to `components`.
121 | - **BREAKING**: `propsDescription` now takes the components' name as a key.
122 | - Massive changes to UI.
123 |
124 | ### Removed
125 |
126 | - **BREAKING**: Support for SB3.
127 | - `propTablesExclude` option.
128 |
129 | ### Added
130 |
131 | - Support for SB4&5.
132 | - `docsInPanel` option to show the info in an addon panel (enabled by default)
133 | - Added a webpack loader parsing Vue components with vue-docgen-api. Thanks to this feature, you can see info for events and slots, and show info exists in mixins or inherited components.
134 |
135 | ## [0.7.1] - 2019-01-23
136 |
137 | ### Fixed
138 |
139 | - Display class name when prop type is class (PR: [#64](https://github.com/pocka/storybook-addon-vue-info/pull/64))
140 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Shota Fuji
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Deprecated
2 |
3 | This addon is deprecated due to [the retirement of official addon-info](https://github.com/storybookjs/deprecated-addons).
4 |
5 | Storybook now has an alternative addon, called [Docs addon(`addon-docs`)](https://github.com/storybookjs/storybook/tree/next/addons/docs),
6 | which comes with native Vue.js support and automatically props/events/slots documentation powered by vue-docgen-api.
7 |
8 | There will be no feature addition nor bug fixes to this repo. Please use Docs addon instead.
9 |
10 | ### Migration to Docs
11 |
12 | As described above, Docs addon uses `vue-docgen-api` via `vue-docgen-loader`.
13 | They are also the tools `storybook-addon-vue-info` uses internally.
14 | So the migration steps is quite simple.
15 |
16 | #### docgen tools are no longer `peerDependencies`
17 |
18 | Since the Docs addon specifies `vue-docgen-api` and `vue-docgen-loader` as direct dependency,
19 | you don't have to list them in your `package.json`.
20 |
21 | ```diff
22 | "dependencies": {
23 | - "vue-docgen-api": "x.x.x",
24 | - "vue-docgen-loader": "x.x.x"
25 | }
26 | ```
27 |
28 | Of course, you can keep them to controll which exact versions to use.
29 |
30 | #### Explicitly specify which component to documentate
31 |
32 | You need to set `component` field in your story metadata.
33 |
34 | ```js
35 | // foo.stories.js
36 | import MyComponent from './my-component.vue'
37 |
38 | export default {
39 | title: 'Components/MyComponent',
40 | component: MyComponent
41 | }
42 |
43 | export const story = () => ({
44 | components: { MyComponent },
45 | template: ''
46 | })
47 | ```
48 |
49 | #### Move `summary` inside a JSDoc comment or MDX
50 |
51 | `summary` option equivalent in Docs addon is component comments or MDX.
52 | Docs addon reads a component comment and displays it as a description for the component.
53 |
54 | ```js
55 | // legacy.stories.js
56 | export const myStory = () => ({
57 | /* ... */
58 | })
59 |
60 | myStory.story = {
61 | info: {
62 | summary: 'foo bar'
63 | }
64 | }
65 | ```
66 |
67 | ```html
68 |
69 |
77 | ```
78 |
79 | Or you can use [MDX](https://github.com/storybookjs/storybook/tree/next/addons/docs#mdx) for more complicated usage.
80 |
81 | ---
82 |
83 |
84 |
85 |

86 |
87 |
88 | [](https://travis-ci.com/pocka/storybook-addon-vue-info)
89 | [](https://badge.fury.io/js/storybook-addon-vue-info)
90 | [](https://www.npmjs.com/package/storybook-addon-vue-info)
91 | [](https://github.com/pocka/storybook-addon-vue-info/blob/master/LICENSE)
92 | [](https://github.com/prettier/prettier)
93 |
94 |
95 |
96 |
97 |
98 | ## storybook-addon-vue-info
99 |
100 | A Storybook addon that shows Vue component's information.
101 |
102 | - [Demo][live examples]
103 |
104 | ## Requirements
105 |
106 | - `@storybook/vue>=4.0.0`
107 |
108 | ## Getting started
109 |
110 | First, install the addon.
111 |
112 | ```sh
113 | npm install --save-dev storybook-addon-vue-info
114 |
115 | # yarn add -D storybook-addon-vue-info
116 | ```
117 |
118 | Then register in `addons.js`.
119 |
120 | ```js
121 | // .storybook/addons.js
122 |
123 | // Don't forget "/lib/" !!
124 | import 'storybook-addon-vue-info/lib/register'
125 | ```
126 |
127 | And setup a webpack loader in order to extract component information with [vue-docgen-api](https://github.com/vue-styleguidist/vue-docgen-api).
128 |
129 | ```sh
130 | npm install --save-dev vue-docgen-loader vue-docgen-api
131 |
132 | # yarn add -D vue-docgen-loader vue-docgen-api
133 | ```
134 |
135 | ```js
136 | // .storybook/webpack.config.js
137 |
138 | // This example uses "Full control mode + default".
139 | // If you are using other mode, add payload of `config.module.rules.push` to rules list.
140 | module.exports = ({ config }) => {
141 | config.module.rules.push({
142 | test: /\.vue$/,
143 | loader: 'vue-docgen-loader',
144 | enforce: 'post'
145 | })
146 |
147 | return config
148 | }
149 | ```
150 |
151 | ## Usage
152 |
153 | Add `withInfo` decorator then set `info` options to the story.
154 |
155 | NOTE: `info` option is required for the addon. If you omit it, the addon does nothing.
156 |
157 | ```js
158 | import { storiesOf } from '@storybook/vue'
159 |
160 | import { withInfo } from 'storybook-addon-vue-info'
161 |
162 | storiesOf('MyComponent', module)
163 | .addDecorator(withInfo)
164 | .add(
165 | 'foo',
166 | () => ({
167 | components: { MyAwesomeComponent },
168 | template: ''
169 | }),
170 | {
171 | info: {
172 | summary: 'Summary for MyComponent'
173 | }
174 | }
175 | )
176 | ```
177 |
178 | You can set the addon as global decorator.
179 |
180 | ```js
181 | // config.js
182 | import { addDecorator } from '@storybook/vue'
183 |
184 | import { withInfo } from 'storybook-addon-vue-info'
185 |
186 | addDecorator(withInfo)
187 | ```
188 |
189 | To set default options, use `setDefaults`.
190 |
191 | ```js
192 | // .storybook/config.js
193 | import { setDefaults } from 'storybook-addon-vue-info'
194 |
195 | setDefaults({
196 | header: false
197 | })
198 | ```
199 |
200 | For more details, see [live examples].
201 |
202 | ## Options
203 |
204 | | Name | Data type | Default value | Description |
205 | | ------------------ | --------------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------- |
206 | | `header` | `boolean` | `true` | Whether to show header or not. |
207 | | `source` | `boolean` | `true` | Whether to show source(usage) or not. |
208 | | `wrapperComponent` | `Component` | [default wrapper](src/components/Wrapper/index.vue) | Override inline docs component. |
209 | | `previewClassName` | `string` | `undefined` | Class name passed down to preview container. |
210 | | `previewStyle` | Style object | `undefined` | Style passed down to preview container. |
211 | | `summary` | `string` | `''` | Summary for the story. Accepts Markdown. |
212 | | `components` | `{ [name: string]: Component }\|null` | `null` | Display info for these components. Same type as component's `components` property. |
213 | | `docsInPanel` | `boolean` | `true` | Whether to show docs in addon panel. |
214 | | `useDocgen` | `boolean` | `true` | Whether to use result of vue-docgen-api. |
215 | | `casing` | `"kebab" \| "camel" \| "pascal" \| undefined` | `undefined` | Which case to use. For detailed usage, see below. |
216 |
217 | ### Valid `casing` options
218 |
219 | ```js
220 | {
221 | // Don't convert names
222 | casing: undefined
223 | }
224 |
225 | {
226 | // Show names in kebab-case
227 | casing 'kebab'
228 | }
229 |
230 | {
231 | // Show prop names in camelCase and
232 | // Show component names in PascalCase
233 | casing: 'camel' // or 'pascal'
234 | }
235 |
236 | {
237 | // Show prop names in camelCase and
238 | // Show component names in kebab-case
239 | casing: {
240 | props: 'camel',
241 | component: 'kebab'
242 | }
243 | }
244 | ```
245 |
246 | In addition to addon options, we have a component option.
247 |
248 | ### Set descriptions manually
249 |
250 | With vue-docgen-api, the addon automatically shows descriptions and types extracted by docgen ([see example in vue-docgen-api README](https://github.com/vue-styleguidist/vue-styleguidist/tree/dev/packages/vue-docgen-api#example)).
251 | However, if you want to explicitly specify desciprion for component props, events or slots, add `description` option for your story component.
252 |
253 | Assume `` have props `label` and `visible`.
254 |
255 | ```js
256 | storiesOf('MyComponent', module)
257 | .addDecorator(withInfo)
258 | .add(
259 | 'foo',
260 | () => ({
261 | components: { MyAwesomeComponent },
262 | template: '',
263 | description: {
264 | MyAwesomeComponent: {
265 | props: {
266 | // These description will appear in `description` column in props table
267 | label: 'A label for my awesome component',
268 | visible: 'Whether component is visible or not'
269 | },
270 | events: {
271 | click: 'Event for user clicking the component'
272 | },
273 | slots: {
274 | default: 'Place text or icon here'
275 | }
276 | }
277 | }
278 | }),
279 | {
280 | info: true
281 | }
282 | )
283 | ```
284 |
285 | For more detail, please take a look at [live example](https://storybook-addon-vue-info.netlify.com/?path=/story/examples-advance-usage--set-descriptions-manually).
286 |
287 | ## Example
288 |
289 | For real example, see `example` directory.
290 |
291 | [live examples]: https://storybook-addon-vue-info.netlify.com/?path=/story/examples-basic-usage--simple-example
292 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pocka/storybook-addon-vue-info/59b662a48575c685895acc06bfcc86cb4db19b33/assets/logo.png
--------------------------------------------------------------------------------
/example/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "transform-vue-jsx"
4 | ],
5 | "presets": ["vue"]
6 | }
7 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | package-lock.json
2 |
--------------------------------------------------------------------------------
/example/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import 'storybook-addon-vue-info/lib/register'
2 |
3 | import '@storybook/addon-a11y/register'
4 | import '@storybook/addon-knobs/register'
5 | import '@storybook/addon-backgrounds/register'
6 | import '@storybook/addon-viewport/register'
7 | import '@storybook/addon-storysource/register'
8 |
--------------------------------------------------------------------------------
/example/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure, addDecorator } from '@storybook/vue'
2 |
3 | import Vue from 'vue'
4 | import VueI18n from 'vue-i18n'
5 |
6 | import { withInfo } from 'storybook-addon-vue-info'
7 |
8 | import BaseButton from '../components/BaseButton.vue'
9 |
10 | Vue.component('base-button', BaseButton)
11 |
12 | Vue.use(VueI18n)
13 |
14 | addDecorator(withInfo)
15 |
16 | configure(require.context('../stories', true, /\.stories\.js$/), module)
17 |
--------------------------------------------------------------------------------
/example/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = ({ config }) => {
4 | config.resolve.alias = {
5 | ...config.resolve.alias,
6 | '~': path.resolve(__dirname, '../stories')
7 | }
8 |
9 | config.module.rules.push({
10 | test: /\.vue$/,
11 | loader: 'vue-docgen-loader',
12 | options: {
13 | docgenOptions: {
14 | alias: config.resolve.alias
15 | }
16 | },
17 | enforce: 'post'
18 | })
19 |
20 | config.module.rules.push({
21 | test: /\.js$/,
22 | resourceQuery: /component/,
23 | loader: 'vue-docgen-loader',
24 | enforce: 'post'
25 | })
26 |
27 | config.module.rules.push({
28 | test: /\.stories\.js$/,
29 | loaders: [require.resolve('@storybook/addon-storysource/loader')],
30 | enforce: 'pre'
31 | })
32 |
33 | return config
34 | }
35 |
--------------------------------------------------------------------------------
/example/components/BaseBlank.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/components/BaseButton.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
41 |
42 |
43 |
67 |
--------------------------------------------------------------------------------
/example/components/NumberList.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 | -
22 | {{n}}
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/example/components/customDocs/wrapper/Wrapper.css:
--------------------------------------------------------------------------------
1 | .savi-wrapper {
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 |
8 | overflow: auto;
9 | color: #fff;
10 | background-color: #d5287b;
11 | font-family: Roboto, sans-serif;
12 | }
13 |
14 | .header {
15 | padding: 32px;
16 | padding-bottom: 60px;
17 | }
18 |
19 | .title {
20 | margin: 0;
21 | font-size: 32px;
22 | line-height: 1.1875;
23 |
24 | font-weight: 500;
25 | }
26 |
27 | .subtitle {
28 | margin: 0;
29 | margin-top: 8px;
30 | font-size: 24px;
31 | line-height: 1.208;
32 |
33 | font-weight: 400;
34 |
35 | opacity: 0.85;
36 | }
37 |
38 | .preview-container {
39 | padding: 32px;
40 |
41 | background-color: #fff;
42 | }
43 |
44 | .preview {
45 | all: initial;
46 | display: block;
47 | position: relative;
48 | }
49 |
50 | .info-body {
51 | padding: 32px;
52 | }
53 |
54 | .info-body > :not(:first-child) {
55 | padding-top: 32px;
56 | }
57 |
58 | .heading {
59 | margin: 0;
60 | font-size: 24px;
61 | line-height: 1.208;
62 |
63 | font-weight: 500;
64 | }
65 |
66 | .subheading {
67 | margin: 0;
68 | font-size: 16px;
69 | line-height: 1.1875;
70 |
71 | font-weight: 500;
72 | }
73 |
74 | .codeblock {
75 | margin: 0;
76 | margin-top: 16px;
77 | padding: 8px 16px;
78 | font-size: 12px;
79 |
80 | background-color: #444;
81 | border-radius: 4px;
82 | }
83 |
84 | .component > :not(:first-child) {
85 | margin-top: 16px;
86 | }
87 |
88 | .list {
89 | margin: 0;
90 | margin-top: 8px;
91 | padding: 0;
92 |
93 | list-style: none;
94 | }
95 |
96 | .item:not(:first-child) {
97 | margin-top: 8px;
98 | }
99 | .item::before {
100 | content: '-';
101 | }
102 |
--------------------------------------------------------------------------------
/example/components/customDocs/wrapper/Wrapper.vue:
--------------------------------------------------------------------------------
1 |
85 |
86 |
87 |
88 |
92 |
95 |
96 |
97 |
98 |
Usage
99 |
{{ info.storySource }}
103 |
104 |
105 |
<{{c.name}}> component
106 |
107 |
Props
108 |
109 | -
114 | {{getPropText(p)}}
115 |
116 |
117 |
118 |
119 |
Events
120 |
121 | -
126 | {{getEventText(e)}}
127 |
128 |
129 |
130 |
131 |
Slots
132 |
133 | -
138 | {{getSlotText(s)}}
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "0.1.1",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "dev": "webpack-dev-server --hot --host 0.0.0.0 --port 8080",
9 | "storybook": "start-storybook -p 8080 -c .storybook",
10 | "build": "build-storybook -c .storybook -o build"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/pocka/storybook-addon-vue-info.git"
15 | },
16 | "author": "pocka",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/pocka/storybook-addon-vue-info/issues"
20 | },
21 | "homepage": "https://github.com/pocka/storybook-addon-vue-info#readme",
22 | "dependencies": {
23 | "storybook-addon-vue-info": "file:..",
24 | "vue": "^2.5.22"
25 | },
26 | "devDependencies": {
27 | "@babel/core": "^7.2.2",
28 | "@storybook/addon-a11y": "^5.0.0",
29 | "@storybook/addon-backgrounds": "^5.0.0",
30 | "@storybook/addon-knobs": "^5.0.0",
31 | "@storybook/addon-storysource": "^5.0.0",
32 | "@storybook/addon-viewport": "^5.0.0",
33 | "@storybook/vue": "^5.0.0",
34 | "babel-loader": "^8.0.5",
35 | "babel-plugin-syntax-jsx": "^6.18.0",
36 | "babel-plugin-transform-vue-jsx": "^3.7.0",
37 | "babel-preset-vue": "^2.0.2",
38 | "highlight.js": "^9.14.1",
39 | "marked": "^0.6.0",
40 | "vue-docgen-api": "^4.0.0",
41 | "vue-docgen-loader": "^1.3.0-alpha.1",
42 | "vue-i18n": "^8.8.0",
43 | "vue-loader": "^15.6.1",
44 | "vue-template-compiler": "^2.5.22",
45 | "webpack": "^4.29.0",
46 | "webpack-dev-server": "^3.1.14"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/example/stories/compatibilities/official/a11y.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import { withA11y } from '@storybook/addon-a11y'
4 |
5 | import MyButton from './components/MyButton.vue'
6 |
7 | /**
8 | * a11y addon does not show result until we change addon panel tab.
9 | * I don't know this is our bug or addon-a11y's one...
10 | */
11 |
12 | storiesOf('Compatibilities/@storybook/addon-a11y', module)
13 | .addDecorator(withA11y)
14 | .add(
15 | 'panel',
16 | () => ({
17 | components: { MyButton },
18 | template: 'FOO'
19 | }),
20 | {
21 | info: {}
22 | }
23 | )
24 | .add(
25 | 'inline',
26 | () => ({
27 | components: { MyButton },
28 | template: 'FOO'
29 | }),
30 | {
31 | info: {
32 | docsInPanel: false
33 | }
34 | }
35 | )
36 |
--------------------------------------------------------------------------------
/example/stories/compatibilities/official/backgrounds.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import MyButton from './components/MyButton.vue'
4 |
5 | storiesOf('Compatibilities/@storybook/addon-backgrounds', module)
6 | .addParameters({
7 | backgrounds: [
8 | { name: 'twitter', value: '#00aced' },
9 | { name: 'facebook', value: '#3b5998' },
10 | { name: 'normal', value: '#fff', default: true }
11 | ]
12 | })
13 | .add(
14 | 'panel',
15 | () => ({
16 | components: { MyButton },
17 | template: 'FOO'
18 | }),
19 | {
20 | info: {}
21 | }
22 | )
23 | .add(
24 | 'inline',
25 | () => ({
26 | components: { MyButton },
27 | template: 'FOO'
28 | }),
29 | {
30 | info: {
31 | docsInPanel: false
32 | }
33 | }
34 | )
35 |
--------------------------------------------------------------------------------
/example/stories/compatibilities/official/components/MyButton.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
26 |
--------------------------------------------------------------------------------
/example/stories/compatibilities/official/knobs.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import { withKnobs, text } from '@storybook/addon-knobs'
4 |
5 | import MyButton from './components/MyButton.vue'
6 |
7 | /**
8 | * Vue.js + addon-knobs has no-updating issue now.
9 | * Stories here written with a workaround described in the issue.
10 | * https://github.com/storybooks/storybook/issues/5129
11 | */
12 |
13 | storiesOf('Compatibilities/@storybook/addon-knobs', module)
14 | .addDecorator(withKnobs)
15 | .add(
16 | 'panel',
17 | () => ({
18 | props: {
19 | label: {
20 | type: String,
21 | default: text('Label', 'foo')
22 | }
23 | },
24 | components: { MyButton },
25 | template: '{{label}}'
26 | }),
27 | {
28 | info: {}
29 | }
30 | )
31 | .add(
32 | 'inline',
33 | () => ({
34 | props: {
35 | label: {
36 | type: String,
37 | default: text('Label', 'foo')
38 | }
39 | },
40 | components: { MyButton },
41 | template: '{{label}}'
42 | }),
43 | {
44 | info: { docsInPanel: false }
45 | }
46 | )
47 |
--------------------------------------------------------------------------------
/example/stories/examples/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import BaseBlank from '../../components/BaseBlank.vue'
4 | import BaseButton from '../../components/BaseButton.vue'
5 | import NumberList from '../../components/NumberList.vue'
6 |
7 | import CustomWrapper from '../../components/customDocs/wrapper/Wrapper.vue'
8 |
9 | storiesOf('Examples/Basic usage', module)
10 | .add(
11 | 'Simple example',
12 | () => ({
13 | components: { LocalButton: BaseButton },
14 | template: ''
15 | }),
16 | {
17 | info: {}
18 | }
19 | )
20 | .add(
21 | 'Show summary',
22 | () => ({
23 | components: { BaseButton },
24 | template: ''
25 | }),
26 | {
27 | info: `
28 | # This is _summary_
29 |
30 | You can write summary in [Markdown](https://en.wikipedia.org/wiki/Markdown).
31 |
32 | There is also syntax highlighting powered by [Highlight.js](https://highlightjs.org/).
33 |
34 | \`\`\`js
35 | export function foo() {
36 | console.log('Hello, World!')
37 | }
38 | \`\`\`
39 | `
40 | }
41 | )
42 | .add(
43 | 'Multiple components',
44 | () => ({
45 | components: { BaseButton, NumberList },
46 | template: `
47 |
48 |
49 |
50 |
51 | `
52 | }),
53 | {
54 | info: 'You can specify more than one components to show info tables.'
55 | }
56 | )
57 | .add(
58 | 'Specify components',
59 | () => ({
60 | components: { BaseButton, NumberList, BaseBlank },
61 | template: `
62 |
63 |
64 |
65 |
66 |
67 | `
68 | }),
69 | {
70 | info: {
71 | summary: `
72 | You can specify which components to be shown in info tables by passing \`components\` option to the addon.
73 |
74 | \`\`\`js
75 | storiesOf('...')
76 | .add('...', () => ({
77 | // Addon ignore this
78 | components: { BaseButton, NumberList, BaseBlank },
79 | template: '...'
80 | }), {
81 | info: {
82 | // Show tables for these
83 | components: { BaseButton, NumberList }
84 | }
85 | })
86 | \`\`\`
87 | `,
88 | components: { BaseButton, NumberList }
89 | }
90 | }
91 | )
92 |
93 | storiesOf('Examples/Advance usage', module)
94 | .add(
95 | 'Hide header',
96 | () => ({
97 | components: { BaseButton },
98 | template: ''
99 | }),
100 | {
101 | info: {
102 | summary: `
103 | To hide header section, set \`header\` option to \`false\`.
104 |
105 | \`\`\`js
106 | {
107 | info: {
108 | header: false
109 | }
110 | }
111 | \`\`\`
112 | `,
113 | header: false
114 | }
115 | }
116 | )
117 | .add(
118 | 'Hide story source',
119 | () => ({
120 | components: { BaseButton },
121 | template: ''
122 | }),
123 | {
124 | info: {
125 | summary: `
126 | To hide story source, set \`source\` option to \`false\`.
127 |
128 | \`\`\`js
129 | {
130 | info: {
131 | source: false
132 | }
133 | }
134 | \`\`\`
135 | `,
136 | source: false
137 | }
138 | }
139 | )
140 | .add(
141 | 'Ignore docgen info',
142 | () => ({
143 | components: { BaseButton },
144 | template: ''
145 | }),
146 | {
147 | info: {
148 | summary: `
149 | To ignore component's information generated by vue-docgen-api,
150 | set \`useDocgen\` option to \`false\`.
151 | This only affects when you set our custom loader in Storybook's webpack config.
152 |
153 | \`\`\`js
154 | {
155 | info: {
156 | useDocgen: false
157 | }
158 | }
159 | \`\`\`
160 | `,
161 | useDocgen: false
162 | }
163 | }
164 | )
165 | .add(
166 | 'Set descriptions manually',
167 | () => ({
168 | components: { BaseButton },
169 | template: '',
170 | description: {
171 | BaseButton: {
172 | props: {
173 | disabled: 'DISABLED!',
174 | type: 'TYPE!',
175 | label: 'LABEL!'
176 | },
177 | events: {
178 | click: 'AWESOME CLICK EVENT!'
179 | },
180 | slots: {
181 | default: "DEFAULT SLOT, THAT'S ALL!"
182 | }
183 | }
184 | }
185 | }),
186 | {
187 | info: {
188 | summary: `
189 | You can set description for each props explicitly.
190 |
191 | \`\`\`js
192 | () => ({
193 | components: { BaseButton },
194 | template: '',
195 | description: {
196 | BaseButton: {
197 | props: {
198 | disabled: 'DISABLED!',
199 | type: 'TYPE!',
200 | label: 'LABEL!'
201 | },
202 | events: {
203 | click: 'AWESOME CLICK EVENT!'
204 | },
205 | slots: {
206 | default: 'DEFAULT SLOT, THAT\'S ALL!'
207 | }
208 | }
209 | }
210 | })
211 | \`\`\`
212 |
213 | This will override descriptions generated by docgen.
214 |
215 | ---
216 |
217 | **CAUTION!**
218 |
219 | If you write descriptions for events and slots without using vue-docgen-api,
220 | the addon shows the description no matter whether the slot or event exists.
221 | The reason is we cannot know what events and slots a component have:
222 | so the addon shows attributes user wrote as "information acquired in runtime".
223 | `
224 | }
225 | }
226 | )
227 | .add(
228 | 'Show docs in preview area',
229 | () => ({
230 | components: { BaseButton },
231 | template: ''
232 | }),
233 | {
234 | info: {
235 | summary: `
236 | To show docs in preview area, turn off \`docsInPanel\` option.
237 |
238 | \`\`\`js
239 | {
240 | info: {
241 | docsInPanel: false
242 | }
243 | }
244 | \`\`\`
245 |
246 | This will override descriptions generated by docgen.
247 |
248 | If you never use docsInPanel feature,
249 | you can omit \`import 'storybook-addon-vue-info/register\` in \`.storybook/addons.js\`.
250 |
251 | ### ATTENTION!
252 |
253 | Docs in preview wraps story component, which may cause unexpected behaviors/bugs.
254 | I personally recommend to use \`docsInPanel: true\`.
255 | `,
256 | docsInPanel: false
257 | }
258 | }
259 | )
260 | .add(
261 | 'Apply custom styles to preview',
262 | () => ({
263 | components: { BaseButton },
264 | template: ''
265 | }),
266 | {
267 | info: {
268 | docsInPanel: false,
269 | previewClassName: 'my-custom-class-name',
270 | previewStyle: {
271 | backgroundColor: 'tomato',
272 | padding: '1em'
273 | },
274 | summary: `
275 | You can customize preview wrapper style by
276 | passing \`previewClassName\` option or \`previewStyle\` option.
277 | `
278 | }
279 | }
280 | )
281 | .add(
282 | 'Customize docs',
283 | () => ({
284 | components: { BaseButton },
285 | template: ''
286 | }),
287 | {
288 | info: {
289 | summary: `
290 | ## Customize docs in preview
291 |
292 | To customize docs in preview area, set your docs component to \`wrapperComponent\` option.
293 |
294 | This addon passes two props:
295 |
296 | - \`info\` ... Information about story and contained components. Interface defined in \`src/types/info\` (\`StoryInfo\`).
297 | - \`options\` ... Addon options.
298 |
299 | Story component is passed as default slot.
300 |
301 | For more detail, please look at source code of this example
302 | (\`example/components/customDocs/wrapper\`).
303 |
304 |
305 |
306 | ## Customize docs in addon panel
307 |
308 | Not supported.
309 |
310 | It's challenging to change components in addon panel.
311 | To avoid plugin being complex and messy, customizing addon panel is not supported.
312 | `,
313 | wrapperComponent: CustomWrapper,
314 | docsInPanel: false
315 | }
316 | }
317 | )
318 | .add(
319 | 'Writes story in JSX',
320 | () => ({
321 | render(h) {
322 | return
323 | }
324 | }),
325 | {
326 | info: {
327 | summary: `
328 | You can also write story in render function with JSX.
329 | Of course you should configure Babel.
330 |
331 | \`\`\`jsx
332 | () => ({
333 | render(h) {
334 | return
335 | }
336 | })
337 | \`\`\`
338 | `
339 | }
340 | }
341 | )
342 |
--------------------------------------------------------------------------------
/example/stories/issues/119/component.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | const JsButton = Vue.extend({
4 | props: {
5 | /**
6 | * My awesome props!
7 | */
8 | awesome: {
9 | type: String,
10 | default: 'foo'
11 | }
12 | },
13 | template: ''
14 | })
15 |
16 | export { JsButton }
17 |
--------------------------------------------------------------------------------
/example/stories/issues/119/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import { JsButton as Issue119 } from './component.js?component'
4 |
5 | storiesOf('Issues/#119', module).add(
6 | 'Parse non-SFC component with docgen',
7 | () => ({
8 | components: { Issue119 },
9 | template: ''
10 | }),
11 | {
12 | info: {
13 | summary: ''
14 | }
15 | }
16 | )
17 |
--------------------------------------------------------------------------------
/example/stories/issues/122/camel.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | camelCase
14 |
15 |
--------------------------------------------------------------------------------
/example/stories/issues/122/index.stories.js:
--------------------------------------------------------------------------------
1 | import CamelComponent from './camel.vue'
2 | import KebabComponent from './kebab.vue'
3 | import MixedComponent from './mixed.vue'
4 |
5 | export default {
6 | title: 'Issues/#122'
7 | }
8 |
9 | export const camelToKebab = () => {
10 | return {
11 | components: { CamelComponent },
12 | template: ''
13 | }
14 | }
15 | camelToKebab.story = {
16 | title: 'Convert camelCase to kebab-case',
17 | parameters: {
18 | info: {
19 | casing: 'kebab'
20 | }
21 | }
22 | }
23 |
24 | export const kebabToCamel = () => {
25 | return {
26 | components: { KebabComponent },
27 | template: ''
28 | }
29 | }
30 | kebabToCamel.story = {
31 | title: 'Convert kebab-case to camelCase',
32 | parameters: {
33 | info: {
34 | casing: 'camel'
35 | }
36 | }
37 | }
38 |
39 | export const mixedToKebab = () => {
40 | return {
41 | components: { MixedComponent },
42 | template: ''
43 | }
44 | }
45 | mixedToKebab.story = {
46 | title: 'Convert to kebab-case (mixed)',
47 | parameters: {
48 | info: {
49 | casing: 'kebab'
50 | }
51 | }
52 | }
53 |
54 | export const mixedToCamel = () => {
55 | return {
56 | components: { MixedComponent },
57 | template: ''
58 | }
59 | }
60 | mixedToCamel.story = {
61 | title: 'Convert to camelCase (mixed)',
62 | parameters: {
63 | info: {
64 | casing: 'camel'
65 | }
66 | }
67 | }
68 |
69 | export const preserve = () => {
70 | return {
71 | components: { MixedComponent },
72 | template: ''
73 | }
74 | }
75 | preserve.story = {
76 | title: 'Preserve casing when undefined',
77 | parameters: {
78 | info: {}
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/example/stories/issues/122/kebab.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | kebab-case
14 |
15 |
--------------------------------------------------------------------------------
/example/stories/issues/122/mixed.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 | mIxeD-CaSE
18 |
19 |
--------------------------------------------------------------------------------
/example/stories/issues/126/component.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/example/stories/issues/126/index.stories.js:
--------------------------------------------------------------------------------
1 | import Issue126 from './component.vue'
2 |
3 | export default {
4 | title: 'Issues/#126'
5 | }
6 |
7 | export const showDefaultValue = () => {
8 | return {
9 | components: { Issue126 },
10 | template: ''
11 | }
12 | }
13 |
14 | showDefaultValue.story = {
15 | title: 'Show default value without docgen',
16 | parameters: {
17 | info: {
18 | useDocgen: false
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/example/stories/issues/21/BulmaButton.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 | BUTTON
26 |
27 |
--------------------------------------------------------------------------------
/example/stories/issues/21/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import BulmaButton from './BulmaButton'
4 |
5 | storiesOf('Issues/#21', module).add(
6 | 'Do not reset preview style',
7 | () => ({
8 | components: { BulmaButton },
9 | template: ''
10 | }),
11 | {
12 | info: {
13 | docsInPanel: false,
14 | summary: ''
15 | }
16 | }
17 | )
18 |
--------------------------------------------------------------------------------
/example/stories/issues/50/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import BaseButton from '../../../components/BaseButton.vue'
4 |
5 | storiesOf('Issues/#50', module)
6 | .addDecorator(() => ({
7 | template: '
'
8 | }))
9 | .add(
10 | 'Do not show decorators',
11 | () => ({
12 | components: { BaseButton },
13 | template: ''
14 | }),
15 | {
16 | info: {
17 | summary: ''
18 | }
19 | }
20 | )
21 |
--------------------------------------------------------------------------------
/example/stories/issues/53/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import NumberList from '../../../components/NumberList.vue'
4 |
5 | storiesOf('Issues/#53', module).add(
6 | 'Should load tables even for PascalCase components',
7 | () => ({
8 | components: { NumberList },
9 | template: ''
10 | }),
11 | {
12 | info: {
13 | summary: '',
14 | useDocgen: false
15 | }
16 | }
17 | )
18 |
--------------------------------------------------------------------------------
/example/stories/issues/59/child.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | {{ foo }}
11 |
12 |
--------------------------------------------------------------------------------
/example/stories/issues/59/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import Issue59 from './child.vue'
4 |
5 | storiesOf('Issues/#59', module).add(
6 | 'Should load extended component properly',
7 | () => ({
8 | components: { Issue59 },
9 | template: ''
10 | }),
11 | {
12 | info: ''
13 | }
14 | )
15 |
--------------------------------------------------------------------------------
/example/stories/issues/59/parent.vue:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/example/stories/issues/63/component.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/example/stories/issues/63/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import VueI18n from 'vue-i18n'
4 |
5 | import Issue63 from './component.vue'
6 |
7 | storiesOf('Issues/#63', module).add(
8 | 'Should display error message when failed to get default',
9 | () => ({
10 | components: { Issue63 },
11 | i18n: new VueI18n({
12 | locale: 'en',
13 | messages: {
14 | en: {
15 | hello: 'Hello, World'
16 | }
17 | }
18 | }),
19 | template: ''
20 | }),
21 | {
22 | info: {
23 | summary: '',
24 | useDocgen: false
25 | }
26 | }
27 | )
28 |
--------------------------------------------------------------------------------
/example/stories/issues/71/component.vue:
--------------------------------------------------------------------------------
1 |
2 | button
3 |
4 |
--------------------------------------------------------------------------------
/example/stories/issues/71/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import Issue71 from './component.vue'
4 |
5 | storiesOf('Issues/#71', module).add(
6 | 'Should not emit error when component has no props',
7 | () => ({
8 | components: { Issue71 },
9 | template: ''
10 | }),
11 | {
12 | info: ''
13 | }
14 | )
15 |
--------------------------------------------------------------------------------
/example/stories/issues/72/component.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/stories/issues/72/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import Issue72 from './component.vue'
4 |
5 | storiesOf('Issues/#72', module).add(
6 | 'Should not emit error when events has no payloads',
7 | () => ({
8 | components: { Issue72 },
9 | template: ''
10 | }),
11 | {
12 | info: ''
13 | }
14 | )
15 |
--------------------------------------------------------------------------------
/example/stories/issues/85/component.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/stories/issues/85/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import Issue85 from './component.vue'
4 |
5 | storiesOf('Issues/#85', module).add(
6 | 'Should show an error when docgen-api failed to parse',
7 | () => ({
8 | components: { Issue85 },
9 | template: ''
10 | }),
11 | {
12 | info: ''
13 | }
14 | )
15 |
--------------------------------------------------------------------------------
/example/stories/issues/85/instance.js:
--------------------------------------------------------------------------------
1 | export default {
2 | computed: {
3 | label() {
4 | return 'Hello, World!'
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/example/stories/issues/86/component.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/stories/issues/86/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import Issue86 from './component.vue'
4 |
5 | storiesOf('Issues/#86', module).add(
6 | 'Configure docgen-api via loader options',
7 | () => ({
8 | components: { Issue86 },
9 | template: ''
10 | }),
11 | {
12 | info: ''
13 | }
14 | )
15 |
--------------------------------------------------------------------------------
/example/stories/issues/86/mixin.vue:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/example/stories/issues/95/component.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/stories/issues/95/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import Issue95 from './component.vue'
4 |
5 | storiesOf('Issues/#95', module)
6 | .add(
7 | 'Should not emit error for no-type props (with docgen)',
8 | () => ({
9 | components: { Issue95 },
10 | template: ''
11 | }),
12 | {
13 | info: ''
14 | }
15 | )
16 | .add(
17 | 'Should not emit error for no-type props (runtime)',
18 | () => ({
19 | components: { Issue95 },
20 | template: ''
21 | }),
22 | {
23 | info: {
24 | summary:
25 | '',
26 | useDocgen: false
27 | }
28 | }
29 | )
30 |
--------------------------------------------------------------------------------
/loader/index.js:
--------------------------------------------------------------------------------
1 | const clone = require('clone')
2 | const docgen = require('vue-docgen-api')
3 | const loaderUtils = require('loader-utils')
4 | const qs = require('querystring')
5 |
6 | console.warn(
7 | 'storybook-addon-vue-info/loader is deprecated. Please consider using vue-docgen-loader instead.'
8 | )
9 |
10 | module.exports = function(content, map) {
11 | const queries = qs.parse(this.resourceQuery.slice(1))
12 |
13 | // Ensure not to apply to each blocks (
48 |
49 |
50 |
51 |
{{ title }}
52 |
53 |
54 |
55 | Name |
56 | Type |
57 | Default |
58 | Description |
59 |
60 |
61 |
62 |
63 |
64 | {{ normalizeCase(prop.name, 'props') }}
65 | *
66 | |
67 | {{ prop.type }} |
68 | {{ prop.default }} |
69 | {{ prop.description }} |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Name |
77 | Type |
78 | Description |
79 |
80 |
81 |
82 |
83 | {{ event.name }} |
84 | {{ event.type }} |
85 | {{ event.description }} |
86 |
87 |
88 |
89 |
90 |
91 |
92 | Name |
93 | Description |
94 |
95 |
96 |
97 |
98 | {{ slot.name }} |
99 | {{ slot.description }} |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/src/components/Component/style.css:
--------------------------------------------------------------------------------
1 | .container {
2 | font-family: Roboto, sans-serif;
3 | }
4 |
5 | .container > :not(:first-child) {
6 | margin-top: 24px;
7 | }
8 |
9 | .title {
10 | font-size: 24px;
11 | margin: 0;
12 |
13 | color: #333;
14 | font-weight: 400;
15 | }
16 |
17 | .required {
18 | color: #a01a1a;
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Docs/index.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/components/Docs/style.css:
--------------------------------------------------------------------------------
1 | .container {
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | padding: 40px;
8 |
9 | overflow: auto;
10 | }
11 |
12 | .container > :not(:first-child) {
13 | margin-top: 40px;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Header/index.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
{{ title }}
19 | {{ subtitle }}
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/components/Header/style.css:
--------------------------------------------------------------------------------
1 | .container {
2 | color: #333;
3 | font-family: Roboto, sans-serif;
4 | }
5 |
6 | .title,
7 | .subtitle {
8 | margin: 0;
9 | }
10 |
11 | .title {
12 | font-size: 24px;
13 |
14 | font-weight: 500;
15 | }
16 |
17 | .subtitle {
18 | font-size: 18px;
19 | margin-top: 8px;
20 |
21 | font-weight: 400;
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/Preview/index.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/Preview/style.css:
--------------------------------------------------------------------------------
1 | .preview {
2 | position: relative;
3 | display: block;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/SectionContainer/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import XSectionContainer from './index.vue'
4 |
5 | storiesOf('Components/SectionContainer', module)
6 | .add('Preview', () => ({
7 | components: { XSectionContainer },
8 | template: `
9 |
10 | I'm contents!
11 | I'm contents!
12 | I'm contents!
13 | I'm contents!
14 | I'm contents!
15 |
16 | `
17 | }))
18 |
--------------------------------------------------------------------------------
/src/components/SectionContainer/index.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/SectionContainer/style.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
6 | .label {
7 | width: 112px;
8 | font-size: 12px;
9 | line-height: 15px;
10 | padding: 4px 0;
11 |
12 | color: #fff;
13 | background-color: #333;
14 | border-radius: 4px 4px 0 0;
15 | font-family: Roboto, sans-serif;
16 | text-transform: uppercase;
17 | text-align: center;
18 | }
19 |
20 | .contents {
21 | padding: 8px;
22 |
23 | border: 1px solid #333;
24 | border-radius: 0 4px 4px 4px;
25 | overflow: hidden;
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/Separator/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/Separator/style.css:
--------------------------------------------------------------------------------
1 | .separator {
2 | border: 0;
3 | border-bottom: 1px solid #333;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/StorySource/index.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
47 | {{ sourceCode }}
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/components/StorySource/style.css:
--------------------------------------------------------------------------------
1 | .codeBlock.codeBlock {
2 | margin: -8px;
3 | padding: 16px;
4 |
5 | overflow: auto;
6 | background-color: #eee;
7 | }
8 |
9 | .codeBlock.codeBlock > code {
10 | font-size: 12px;
11 |
12 | font-family: 'Roboto Mono', monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Summary/index.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/components/Summary/style.css:
--------------------------------------------------------------------------------
1 | .container {
2 | font-family: Roboto, sans-serif;
3 | }
4 |
5 | .container h1 {
6 | font-size: 24px;
7 | }
8 |
9 | .container h2 {
10 | font-size: 22px;
11 | }
12 |
13 | .container h3 {
14 | font-size: 20px;
15 | }
16 |
17 | .container h4 {
18 | font-size: 18px;
19 | }
20 |
21 | .container h5 {
22 | font-size: 16px;
23 | }
24 |
25 | .container h6 {
26 | font-size: 14px;
27 | }
28 |
29 | .container p {
30 | font-size: 14px;
31 | margin: 16px 0;
32 |
33 | color: #333;
34 | }
35 |
36 | .container a {
37 | color: #0d7ccd;
38 | }
39 |
40 | .container.container pre,
41 | .container.container code {
42 | font-size: 12px;
43 | padding: 4px 8px;
44 |
45 | font-family: 'Roboto Mono', monospace;
46 | background-color: #eee;
47 | border-radius: 4px;
48 | }
49 |
50 | .container.container pre {
51 | display: block;
52 | padding: 16px;
53 | margin: 16px 0;
54 | }
55 |
56 | .container.container pre > code {
57 | padding: 0;
58 | background-color: transparent;
59 | border-radius: 0;
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/Table/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import XTable from './index.vue'
4 |
5 | const slotContent = `
6 |
7 |
8 | Col1 |
9 | Col2 |
10 | Col3 |
11 | Col4 |
12 |
13 |
14 |
15 |
16 | item1-1 |
17 | item1-2 |
18 | item1-3 |
19 | item1-4 |
20 |
21 |
22 | item2-1 |
23 | item2-2 |
24 | item2-3 |
25 | item2-4 |
26 |
27 |
28 | item3-1 |
29 | item3-2 |
30 | item3-3 |
31 | item3-4 |
32 |
33 |
34 | `
35 |
36 | storiesOf('Components/Table', module)
37 | .add('Preview', () => ({
38 | components: { XTable },
39 | template: `
40 | ${slotContent}
41 | `
42 | }))
43 | .add('With label', () => ({
44 | components: { XTable },
45 | template: `
46 | ${slotContent}
47 | `
48 | }))
49 |
--------------------------------------------------------------------------------
/src/components/Table/index.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/components/Table/style.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | overflow: hidden;
5 | }
6 |
7 | .label {
8 | font-size: 12px;
9 |
10 | margin-left: 1px;
11 | margin-bottom: 8px;
12 | }
13 |
14 | .contents {
15 | overflow: auto;
16 | border: 1px solid #333;
17 | border-radius: 4px;
18 | }
19 |
20 | .table {
21 | position: relative;
22 | font-size: 12px;
23 | width: 100%;
24 | border-spacing: 0;
25 | }
26 |
27 | .table thead {
28 | color: #fff;
29 | background-color: #333;
30 | text-transform: uppercase;
31 | text-align: left;
32 | }
33 |
34 | .table tbody tr {
35 | position: relative;
36 | }
37 |
38 | .table tbody tr:not(:first-child)::after {
39 | content: '';
40 | position: absolute;
41 | display: block;
42 | left: 8px;
43 | right: 8px;
44 |
45 | border-bottom: 1px solid #333;
46 | }
47 |
48 | .table tbody tr:first-child > td {
49 | padding-top: 16px;
50 | }
51 | .table tbody tr:last-child > td {
52 | padding-bottom: 16px;
53 | }
54 |
55 | .table td:last-child {
56 | width: 100%;
57 |
58 | white-space: initial;
59 | }
60 |
61 | .table th {
62 | padding: 8px;
63 | }
64 | .table th:first-child,
65 | .table td:first-child {
66 | padding-left: 16px;
67 | padding-right: 56px;
68 | }
69 |
70 | .table td {
71 | padding: 8px;
72 | padding-right: 48px;
73 |
74 | white-space: nowrap;
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/Wrapper/index.stories.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue'
2 |
3 | import dedent from 'dedent-tabs'
4 |
5 | import XWrapper from './index.vue'
6 |
7 | storiesOf('Components/Wrapper', module)
8 | .add('Preview', () => ({
9 | components: {
10 | XWrapper
11 | },
12 | computed: {
13 | summary() {
14 | return dedent(`
15 | # Title1
16 | ## Title2
17 | ### Title3
18 | #### Title4
19 | ##### Title5
20 | ###### Title6
21 |
22 | Paragraph.
23 |
24 | Paragraph,
paragrah, [link](#).
25 |
26 | \`\`\`js
27 | export default function foo() {
28 | console.log('Hello, World!')
29 | }
30 | \`\`\`
31 | `)
32 | },
33 | storySource() {
34 | return 'Button'
35 | },
36 | components() {
37 | return [
38 | {
39 | name: 'x-button',
40 | props: [
41 | {
42 | name: 'disabled',
43 | type: 'boolean',
44 | required: false,
45 | description: 'Whether to disable'
46 | },
47 | {
48 | name: 'type',
49 | type: 'string',
50 | required: false,
51 | default: 'normal',
52 | description: 'Button type'
53 | },
54 | {
55 | name: 'label',
56 | type: 'string',
57 | required: true
58 | }
59 | ]
60 | }
61 | ]
62 | },
63 | info() {
64 | return {
65 | title: 'Title',
66 | subtitle: 'Subtitle',
67 | summary: this.summary,
68 | storySource: this.storySource,
69 | jsxStory: false,
70 | components: this.components
71 | }
72 | },
73 | options() {
74 | return {
75 | header: true,
76 | source: true
77 | }
78 | }
79 | },
80 | template: ''
81 | }))
82 |
--------------------------------------------------------------------------------
/src/components/Wrapper/index.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
49 |
54 |
59 |
60 |
61 |
62 |
67 |
68 |
74 |
75 |
76 |
77 |
82 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Component } from './Component/index.vue'
2 | export { default as Docs } from './Docs/index.vue'
3 | export { default as Header } from './Header/index.vue'
4 | export { default as Preview } from './Preview/index.vue'
5 | export { default as SectionContainer } from './SectionContainer/index.vue'
6 | export { default as Separator } from './Separator/index.vue'
7 | export { default as StorySource } from './StorySource/index.vue'
8 | export { default as Summary } from './Summary/index.vue'
9 | export { default as Table } from './Table/index.vue'
10 | export { default as Wrapper } from './Wrapper/index.vue'
11 |
--------------------------------------------------------------------------------
/src/extract/decideTargets.ts:
--------------------------------------------------------------------------------
1 | import { paramCase } from 'change-case'
2 | import Vue, { ComponentOptions } from 'vue'
3 |
4 | import { InfoAddonOptions } from '../options'
5 | import { ComponentRegistory } from '../types/vue'
6 | import * as getTagNames from '../utils/getTagNames'
7 |
8 | import { lookupGlobalComponent, LookupResult } from './lookup'
9 |
10 | export function decideTargets(
11 | story: ComponentOptions,
12 | options: InfoAddonOptions
13 | ): ComponentRegistory {
14 | // Components user set explicitly
15 | if (options.components) {
16 | return normalizeComponents(options.components)
17 | }
18 |
19 | // Components used in story
20 | if (story.components) {
21 | return normalizeComponents(story.components)
22 | }
23 |
24 | // If components property does not exist in options nor story components,
25 | // parse template/render and return components used in template/render.
26 |
27 | if (!story.template && !story.render) {
28 | throw new Error('`template` or `render` must be on component options.')
29 | }
30 |
31 | const tagNames = story.template
32 | ? getTagNames.fromTemplate(story.template).map(s => paramCase(s))
33 | : getTagNames.fromJSX(story.render!)
34 |
35 | const components = tagNames
36 | .map(tagName => lookupGlobalComponent(tagName))
37 | .filter((res): res is LookupResult => !!res)
38 |
39 | const ret: ComponentRegistory = {}
40 |
41 | for (const { name, component } of components) {
42 | ret[name] = component
43 | }
44 |
45 | return normalizeComponents(ret)
46 | }
47 |
48 | const normalizeComponents = (c: ComponentRegistory): ComponentRegistory => {
49 | const ret: ComponentRegistory = {}
50 |
51 | // There are cases component options only exists under "options" property
52 | for (const key of Object.keys(c)) {
53 | ret[key] = (c[key] as any).__docgenInfo
54 | ? c[key]
55 | : (c[key] as any).options || c[key]
56 | }
57 |
58 | return ret
59 | }
60 |
--------------------------------------------------------------------------------
/src/extract/extractDocgenInfo.ts:
--------------------------------------------------------------------------------
1 | import { ComponentInfo } from '../types/info'
2 | import { AnyComponent } from '../types/vue'
3 |
4 | type Extracted = Pick>
5 |
6 | const normalizeAttrs = (attrs: { [name: string]: any } | any[]): any[] =>
7 | attrs instanceof Array ? attrs : Object.keys(attrs).map(key => attrs[key])
8 |
9 | export function extractDocgenInfo(component: AnyComponent): Extracted {
10 | const docs = (component as any).__docgenInfo
11 |
12 | const props = docs.props
13 | ? normalizeAttrs(docs.props).map(prop => {
14 | return {
15 | name: prop.name,
16 | type: prop.type ? prop.type.name : 'any',
17 | required: !!prop.required,
18 | default: prop.defaultValue ? prop.defaultValue.value : undefined,
19 | description: prop.description
20 | }
21 | })
22 | : []
23 |
24 | const events = docs.events
25 | ? normalizeAttrs(docs.events).map(ev => {
26 | const type = ev.type ? ev.type.names.join(', ') : ''
27 |
28 | return {
29 | name: ev.name,
30 | type,
31 | description: ev.description
32 | }
33 | })
34 | : []
35 |
36 | const slots = docs.slots
37 | ? normalizeAttrs(docs.slots).map(slot => {
38 | return {
39 | name: slot.name,
40 | description: slot.description
41 | }
42 | })
43 | : []
44 |
45 | return { props, events, slots }
46 | }
47 |
--------------------------------------------------------------------------------
/src/extract/getProps.ts:
--------------------------------------------------------------------------------
1 | import { PropInfo } from '../types/info'
2 | import { AnyComponent } from '../types/vue'
3 | import { constructorToString } from '../utils/constructorToString'
4 |
5 | export function getProps(component: AnyComponent): PropInfo[] {
6 | // Async component (Promise)
7 | if (
8 | component instanceof Promise ||
9 | ('component' in component && component.component instanceof Promise)
10 | ) {
11 | console.warn(
12 | '[storybook-addon-vue-info] Cannot enumerate props for async component'
13 | )
14 | return []
15 | }
16 |
17 | // Case given Vue instance
18 | if (!('props' in component)) {
19 | return []
20 | }
21 |
22 | const { props } = component
23 |
24 | if (!props) {
25 | return []
26 | }
27 |
28 | return Object.keys(props).map(name => {
29 | const propDef = props[name]
30 |
31 | // If there are no props defined in "prop: Object" sytle,
32 | // Vue does not convert "prop: Constructor" into "prop: Object" style (See #3).
33 | if (typeof propDef === 'function') {
34 | return {
35 | name,
36 | type: constructorToString(propDef),
37 | required: false,
38 | default: undefined
39 | }
40 | }
41 |
42 | let default$
43 |
44 | if (propDef.type === Function && propDef.default) {
45 | default$ = propDef.default.toString()
46 | } else if (typeof propDef.default === 'function') {
47 | try {
48 | default$ = JSON.stringify(propDef.default.apply(component))
49 | } catch (e) {
50 | console.error(e)
51 | console.warn(
52 | `[storybook-addon-vue-info] Failed to get default value for ${name}`
53 | )
54 | default$ = 'Failed to get default value'
55 | }
56 | } else {
57 | default$ =
58 | typeof propDef === 'object' && 'default' in propDef
59 | ? JSON.stringify(propDef.default)
60 | : ''
61 | }
62 |
63 | return {
64 | name,
65 | type: propDef.type ? constructorToString(propDef.type) : 'any',
66 | required: propDef.required,
67 | default: default$
68 | }
69 | })
70 | }
71 |
--------------------------------------------------------------------------------
/src/extract/index.ts:
--------------------------------------------------------------------------------
1 | import { paramCase } from 'change-case'
2 | import dedent from 'dedent-tabs'
3 | import hljs from 'highlight.js'
4 | import marked from 'marked'
5 | import Vue, { ComponentOptions } from 'vue'
6 |
7 | import { InfoAddonOptions } from '../options'
8 | import { ComponentInfo, StoryInfo } from '../types/info'
9 | import { getJSXFromRenderFn } from '../utils/getJSXFromRenderFn'
10 |
11 | import { decideTargets } from './decideTargets'
12 | import { extractDocgenInfo } from './extractDocgenInfo'
13 | import { getProps } from './getProps'
14 |
15 | export function extract(
16 | story: ComponentOptions,
17 | kindName: string,
18 | storyName: string,
19 | options: InfoAddonOptions
20 | ): StoryInfo {
21 | const targets = decideTargets(story, options)
22 |
23 | const descriptions =
24 | getDescriptionsFromStory(story) || formatPropsDescription(story)
25 |
26 | const components = Object.keys(targets).map(name => {
27 | const component = targets[name]
28 | const kebabName = paramCase(name)
29 |
30 | const propDescriptions =
31 | (descriptions[kebabName] && descriptions[kebabName].props) || {}
32 | const eventDescriptions =
33 | (descriptions[kebabName] && descriptions[kebabName].events) || {}
34 | const slotDescriptions =
35 | (descriptions[kebabName] && descriptions[kebabName].slots) || {}
36 |
37 | if (options.useDocgen && '__docgenInfo' in component) {
38 | const partial = extractDocgenInfo(component)
39 |
40 | const hydrateStoryDescription = <
41 | T extends { name: string; description?: string }
42 | >(
43 | defs: T[],
44 | description: Description
45 | ): T[] => {
46 | return defs.map(def => {
47 | if (def.name in description) {
48 | return {
49 | ...def,
50 | description: description[def.name]
51 | }
52 | }
53 |
54 | return def
55 | })
56 | }
57 |
58 | return {
59 | name,
60 | ...partial,
61 | props:
62 | partial.props &&
63 | hydrateStoryDescription(partial.props, propDescriptions),
64 | events:
65 | partial.events &&
66 | hydrateStoryDescription(partial.events, eventDescriptions),
67 | slots:
68 | partial.slots &&
69 | hydrateStoryDescription(partial.slots, slotDescriptions)
70 | }
71 | }
72 |
73 | const props = getProps(component).map(prop => {
74 | if (prop.name in propDescriptions) {
75 | return {
76 | ...prop,
77 | description: propDescriptions[prop.name]
78 | }
79 | }
80 |
81 | return prop
82 | })
83 |
84 | const events = Object.keys(eventDescriptions).map(eventName => {
85 | return {
86 | name: eventName,
87 | description: eventDescriptions[eventName]
88 | }
89 | })
90 |
91 | const slots = Object.keys(slotDescriptions).map(slotName => {
92 | return {
93 | name: slotName,
94 | description: slotDescriptions[slotName]
95 | }
96 | })
97 |
98 | return { name, props, events, slots }
99 | })
100 |
101 | // Set up markdown renderer for summary
102 | const renderer = new marked.Renderer()
103 |
104 | renderer.code = (code, lang) =>
105 | `${
106 | hljs.highlightAuto(code, lang ? [lang] : undefined).value
107 | }
`
108 |
109 | marked.setOptions({ renderer })
110 |
111 | return {
112 | title: kindName,
113 | subtitle: storyName,
114 | summary: marked(dedent(options.summary)),
115 | storySource: dedent(
116 | story.template || getJSXFromRenderFn(story.render! as any)
117 | ),
118 | jsxStory: !!story.render,
119 | components
120 | }
121 | }
122 |
123 | // Description for props, events and methods.
124 | interface Description {
125 | [name: string]: string
126 | }
127 |
128 | // Description object user
129 | interface Descriptions {
130 | [componentName: string]: {
131 | props?: Description
132 | events?: Description
133 | slots?: Description
134 | }
135 | }
136 |
137 | const getDescriptionsFromStory = (story: any): Descriptions | null => {
138 | if (!story.description) {
139 | return null
140 | }
141 |
142 | const ret: Descriptions = {}
143 |
144 | for (const component of Object.keys(story.description)) {
145 | ret[paramCase(component)] = story.description[component]
146 | }
147 |
148 | return ret
149 | }
150 |
151 | const formatPropsDescription = (story: any): Descriptions => {
152 | if (!story.propsDescription) {
153 | return {}
154 | }
155 |
156 | console.warn(
157 | '[storybook-addon-vue-info] `propsDescription` is deprecated. Please consider switching to `description.props`'
158 | )
159 |
160 | const components: Descriptions = {}
161 |
162 | for (const component of Object.keys(story.propsDescription)) {
163 | components[paramCase(component)] = {
164 | props: story.propsDescription[component]
165 | }
166 | }
167 |
168 | return components
169 | }
170 |
--------------------------------------------------------------------------------
/src/extract/lookup.ts:
--------------------------------------------------------------------------------
1 | import { paramCase } from 'change-case'
2 | import Vue from 'vue'
3 |
4 | import { AnyComponent } from '../types/vue'
5 |
6 | export interface LookupResult {
7 | name: string
8 | component: AnyComponent
9 | }
10 |
11 | /**
12 | * Lookup component matched to specified name which registered at global level.
13 | * @param name A name to lookup (must be hyphenate)
14 | */
15 | export function lookupGlobalComponent(name: string): LookupResult | null {
16 | for (const componentName in (Vue as any).options.components) {
17 | if (paramCase(componentName) === name) {
18 | const target = (Vue as any).options.components[componentName]
19 |
20 | return {
21 | name,
22 | component: target
23 | }
24 | }
25 | }
26 |
27 | return null
28 | }
29 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import { makeDecorator } from '@storybook/addons'
4 |
5 | import { extract } from './extract'
6 | import { defaultOptions, InfoAddonOptions } from './options'
7 | import { renderToPanel } from './view'
8 |
9 | export * from './components'
10 |
11 | function findBottomStorybookWraps(w: any) {
12 | while (
13 | w &&
14 | w.options &&
15 | w.options.components &&
16 | w.options.components.story &&
17 | w.options.components.story.options &&
18 | w.options.components.story.options.STORYBOOK_WRAPS
19 | ) {
20 | w = w.options.components.story.options.STORYBOOK_WRAPS
21 | }
22 | return w
23 | }
24 |
25 | function getComponentOptions(story: any) {
26 | if (story.fnOptions && story.fnOptions.STORYBOOK_WRAPS) {
27 | return findBottomStorybookWraps(story.fnOptions.STORYBOOK_WRAPS).options
28 | }
29 | return ((story.componentOptions && story.componentOptions.Ctor) || {}).options
30 | }
31 |
32 | export const withInfo = makeDecorator({
33 | name: 'withInfo',
34 | parameterName: 'info',
35 | skipIfNoParametersOrOptions: true,
36 | wrapper(getStory, context, { parameters }) {
37 | // Normalize options and set defaults
38 | const options = {
39 | ...defaultOptions,
40 | ...(parameters === true
41 | ? {}
42 | : typeof parameters === 'string'
43 | ? { summary: parameters }
44 | : parameters)
45 | }
46 |
47 | return Vue.extend({
48 | render(h) {
49 | const story = h(getStory(context)) as any
50 | const componentOptions = getComponentOptions(story)
51 | const info = extract(
52 | componentOptions,
53 | context.kind,
54 | context.story,
55 | options
56 | )
57 |
58 | if (options.docsInPanel) {
59 | renderToPanel(info, options)
60 | return story
61 | }
62 |
63 | return h(options.wrapperComponent, { props: { info, options } }, [
64 | story
65 | ])
66 | }
67 | })
68 | }
69 | })
70 |
71 | /**
72 | * Set default options.
73 | *
74 | * @param options options to override with
75 | */
76 | export function setDefaults(options: Partial | string): void {
77 | Object.assign(
78 | defaultOptions,
79 | typeof options === 'string' ? { summary: options } : options
80 | )
81 | }
82 |
--------------------------------------------------------------------------------
/src/lib.d.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:ban-types */
2 |
3 | declare module 'vue-template-compiler' {
4 | export interface ASTElement {
5 | attrs?: any[]
6 | attrsList: any[]
7 | attrsMap: { [key: string]: any }
8 | children: ASTElement[]
9 | parent?: ASTElement
10 | plain: boolean
11 | static: boolean
12 | tag?: string
13 | }
14 | export interface CompileResult {
15 | ast?: ASTElement
16 | render: string
17 | staticRenderFns: string[]
18 | errors: string[]
19 | }
20 | export function compile(template: string): CompileResult
21 | }
22 |
23 | declare module 'vuera' {
24 | import * as React from 'react'
25 | import { AsyncComponent, Component } from 'vue'
26 |
27 | interface Props {
28 | component:
29 | | Component
30 | | AsyncComponent
31 | [prop: string]: any
32 | }
33 |
34 | export const VueWrapper: React.SFC
35 | }
36 |
37 | declare module '@storybook/addons' {
38 | import { SFC } from 'react'
39 |
40 | export interface Api {
41 | onStory(callback: Function): () => void
42 | }
43 |
44 | export interface Channel {
45 | on(name: string, handler: Function): void
46 | emit(name: string, payload: any): void
47 |
48 | removeListener(name: string, handler: Function): void
49 | }
50 |
51 | interface Mod {
52 | register(name: string, callback: (api: Api) => any): void
53 |
54 | addPanel(
55 | name: string,
56 | options: {
57 | title: string
58 | render: SFC<{ active: boolean; key: string }>
59 | }
60 | ): void
61 |
62 | getChannel(): Channel
63 | }
64 |
65 | const addons: Mod
66 |
67 | export default addons
68 |
69 | export interface DecoratorOptions {
70 | name: string
71 | parameterName: string
72 | skipIfNoParametersOrOptions?: boolean
73 | wrapper(
74 | getStory: (ctx: any) => any,
75 | ctx: any,
76 | options: { parameters: any }
77 | ): any
78 | }
79 |
80 | export function makeDecorator(opts: DecoratorOptions): any
81 | }
82 |
--------------------------------------------------------------------------------
/src/options.ts:
--------------------------------------------------------------------------------
1 | import { AnyComponent, ComponentRegistory } from './types/vue'
2 |
3 | import DefaultWrapper from './components/Wrapper/index.vue'
4 |
5 | export const defaultOptions: InfoAddonOptions = {
6 | header: true,
7 | source: true,
8 | summary: '',
9 | components: null,
10 | wrapperComponent: DefaultWrapper,
11 | docsInPanel: true,
12 | useDocgen: true,
13 | casing: undefined
14 | }
15 |
16 | type Casing =
17 | | undefined
18 | | 'camel'
19 | | 'camelCase'
20 | | 'kebab'
21 | | 'kebab-case'
22 | | 'pascal'
23 | | 'PascalCase'
24 |
25 | /**
26 | * Addon options
27 | */
28 | export interface InfoAddonOptions {
29 | /**
30 | * Toggles display of header with component name and description
31 | */
32 | header: boolean
33 |
34 | /**
35 | * Displays the source of story Component
36 | */
37 | source: boolean
38 |
39 | /**
40 | * Summary for the Story
41 | */
42 | summary: string
43 |
44 | previewClassName?: string
45 | previewStyle?: ElementCSSInlineStyle
46 |
47 | /**
48 | * Explicitly specify components to display info
49 | */
50 | components: ComponentRegistory | false | null
51 |
52 | /**
53 | * Customize wrapper docs by overriding components
54 | */
55 | wrapperComponent: AnyComponent
56 |
57 | /**
58 | * Whether to show docs in panel instead of wrapping story
59 | */
60 | docsInPanel: boolean
61 |
62 | /**
63 | * Whether to use component infomation generated by vue-docgen-api
64 | */
65 | useDocgen: boolean
66 |
67 | casing:
68 | | Casing
69 | | {
70 | props: Casing
71 | component: Casing
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/register.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Vue, { ComponentOptions } from 'vue'
3 | import { VueWrapper } from 'vuera'
4 |
5 | import addons, { Api, Channel } from '@storybook/addons'
6 |
7 | import { AddonName, PanelName, Events } from './addon'
8 | import { InfoAddonOptions } from './options'
9 | import { StoryInfo } from './types/info'
10 |
11 | import Wrapper from './components/Wrapper/index.vue'
12 |
13 | interface Props {
14 | channel: Channel
15 | api: Api
16 | active: boolean
17 | }
18 |
19 | interface State {
20 | /** Information of current story */
21 | info?: StoryInfo
22 | /** Addon options */
23 | options?: InfoAddonOptions
24 | }
25 |
26 | class Info extends React.Component {
27 | public state: State = {}
28 |
29 | /** Callback to remove event handler */
30 | private stopListen?: () => void
31 |
32 | /**
33 | * Callback for ShowDocs event
34 | */
35 | private onShowDocs = ({ info, options }: State) => {
36 | this.setState({ info, options })
37 | }
38 |
39 | public componentDidMount() {
40 | const { channel, api } = this.props
41 |
42 | channel.on(Events.ShowDocs, this.onShowDocs)
43 |
44 | this.stopListen = api.onStory(() => {
45 | // Clear panel when story changes
46 | this.setState({
47 | info: undefined,
48 | options: undefined
49 | })
50 | })
51 | }
52 |
53 | public render() {
54 | const { info, options } = this.state
55 | const { active } = this.props
56 |
57 | if (!active || !info || !options) {
58 | return null
59 | }
60 |
61 | return (
62 |
63 |
64 |
65 | )
66 | }
67 |
68 | public componentWillUnmount() {
69 | if (this.stopListen) {
70 | this.stopListen()
71 | }
72 |
73 | const { channel } = this.props
74 |
75 | channel.removeListener(Events.ShowDocs, this.onShowDocs)
76 | }
77 | }
78 |
79 | addons.register(AddonName, api => {
80 | addons.addPanel(PanelName, {
81 | title: 'Info(Vue)',
82 | render: ({ active, key }) => (
83 |
84 | )
85 | })
86 | })
87 |
--------------------------------------------------------------------------------
/src/types/dedent-tabs.ts:
--------------------------------------------------------------------------------
1 | declare module 'dedent-tabs'
2 | declare function dedent(literals: string): string
3 | declare function dedent(
4 | literals: TemplateStringsArray,
5 | ...placeholders: any[]
6 | ): string
7 |
--------------------------------------------------------------------------------
/src/types/info.ts:
--------------------------------------------------------------------------------
1 | export interface StoryInfo {
2 | title: string
3 | subtitle: string
4 |
5 | summary?: string
6 |
7 | storySource: string
8 | jsxStory: boolean
9 |
10 | components: ComponentInfo[]
11 | }
12 |
13 | export interface ComponentInfo {
14 | name: string
15 |
16 | props: PropInfo[]
17 | events: EventInfo[]
18 | slots: SlotInfo[]
19 | }
20 |
21 | export interface PropInfo {
22 | name: string
23 | type: string
24 | required: boolean
25 | default?: string
26 | description?: string
27 | }
28 |
29 | export interface EventInfo {
30 | name: string
31 | type?: string
32 | description?: string
33 | }
34 |
35 | export interface SlotInfo {
36 | name: string
37 | description?: string
38 | }
39 |
--------------------------------------------------------------------------------
/src/types/vue.ts:
--------------------------------------------------------------------------------
1 | import { AsyncComponent, Component } from 'vue'
2 |
3 | /**
4 | * Any component, for convenience.
5 | */
6 | export type AnyComponent =
7 | | Component
8 | | AsyncComponent
9 |
10 | /**
11 | * Type of components property.
12 | */
13 | export interface ComponentRegistory {
14 | [name: string]:
15 | | Component
16 | | AsyncComponent
17 | }
18 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue'
3 | export default Vue
4 | }
5 |
6 | declare module '*.css' {
7 | const modules: {
8 | [className: string]: string
9 | }
10 |
11 | export default modules
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/constructorToString.spec.ts:
--------------------------------------------------------------------------------
1 | import constructorToString from './constructorToString'
2 |
3 | it('Label of String to equal "string"', () => {
4 | expect(constructorToString(String)).toBe('string')
5 | })
6 |
7 | it('Label of Number to equal "number"', () => {
8 | expect(constructorToString(Number)).toBe('number')
9 | })
10 |
11 | it('Label of Object to equal "object"', () => {
12 | expect(constructorToString(Object)).toBe('object')
13 | })
14 |
15 | it('Label of Boolean to equal "boolean"', () => {
16 | expect(constructorToString(Boolean)).toBe('boolean')
17 | })
18 |
19 | it('Label of Function to equal "function"', () => {
20 | expect(constructorToString(Function)).toBe('function')
21 | })
22 |
23 | it('Label of Symbol to equal "Symbol"', () => {
24 | expect(constructorToString(Symbol)).toBe('Symbol')
25 | })
26 |
27 | it('Label of Function to equal "function"', () => {
28 | expect(constructorToString(Array)).toBe('array')
29 | })
30 |
31 | it('Label of Class to equal its name', () => {
32 | function MyClass() {}
33 |
34 | expect(constructorToString(MyClass)).toBe('MyClass')
35 | })
36 |
37 | it('Other value will be "unknown"', () => {
38 | expect(constructorToString(() => null)).toBe('unknown')
39 | })
40 |
--------------------------------------------------------------------------------
/src/utils/constructorToString.ts:
--------------------------------------------------------------------------------
1 | type Constructor = () => void
2 |
3 | /**
4 | * Returns type string of constructor.
5 | * @param constructor Constructor function
6 | */
7 | export const constructorToString = (
8 | constructor: Constructor | Constructor[]
9 | ): string => {
10 | if (constructor instanceof Array) {
11 | return constructor.map(constructorToString).join(' | ')
12 | } else if (constructor === Number) {
13 | return 'number'
14 | } else if (constructor === String) {
15 | return 'string'
16 | } else if (constructor === Object) {
17 | return 'object'
18 | } else if (constructor === Boolean) {
19 | return 'boolean'
20 | } else if (constructor === Function) {
21 | return `function`
22 | } else if (constructor === Symbol) {
23 | return 'Symbol'
24 | } else if (constructor === Array) {
25 | return 'array'
26 | } else {
27 | return constructor.name || 'unknown'
28 | }
29 | }
30 |
31 | export default constructorToString
32 |
--------------------------------------------------------------------------------
/src/utils/getJSXFromRenderFn.spec.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import getJSXFromRenderFn, { CreateJSX } from './getJSXFromRenderFn'
4 |
5 | it('First argument for createElement will be tag name', () => {
6 | expect(getJSXFromRenderFn(h => h('div'))).toBe('')
7 |
8 | const MyButton = Vue.component('my-button', {
9 | template: ''
10 | })
11 |
12 | expect(getJSXFromRenderFn(h => h(MyButton))).toBe('')
13 | })
14 |
15 | it('Anonymous component will be shown as ', () => {
16 | const MyLabel = Vue.extend({
17 | template: ''
18 | })
19 |
20 | expect(getJSXFromRenderFn(h => h(MyLabel))).toBe('')
21 |
22 | expect(getJSXFromRenderFn(h => h(MyLabel, [h(MyLabel)]))).toBe(
23 | ''
24 | )
25 | })
26 |
27 | it('Renders children', () => {
28 | const render = (h: CreateJSX) => h('div', [h('span', [h('a')])])
29 |
30 | expect(getJSXFromRenderFn(render)).toBe('')
31 | })
32 |
33 | it('Renderes text node', () => {
34 | expect(getJSXFromRenderFn(h => 'foo')).toBe('foo')
35 |
36 | expect(getJSXFromRenderFn(h => h('p', ['foo']))).toBe('foo
')
37 | })
38 |
39 | it('Renderes attributes and props', () => {
40 | const bar = false
41 |
42 | const render = (h: CreateJSX) =>
43 | h('div', {
44 | props: {
45 | foo: 'foo',
46 | bar
47 | },
48 | attrs: {
49 | label: 'baz'
50 | },
51 | on: {
52 | click: (ev: any) => console.log(ev)
53 | }
54 | })
55 |
56 | expect(getJSXFromRenderFn(render)).toBe(
57 | ''
58 | )
59 | })
60 |
--------------------------------------------------------------------------------
/src/utils/getJSXFromRenderFn.ts:
--------------------------------------------------------------------------------
1 | import { AsyncComponent, Component, VNodeData } from 'vue'
2 |
3 | import { AnyComponent } from '../types/vue'
4 |
5 | export type RenderFn = (h: CreateJSX) => any
6 |
7 | /**
8 | * Get JSX strings from render function.
9 | * @param render
10 | */
11 | export const getJSXFromRenderFn = (render: RenderFn): string => {
12 | return render(createJSX)
13 | }
14 |
15 | export default getJSXFromRenderFn
16 |
17 | export interface CreateJSX {
18 | (tag?: Tag, children?: JSXStringChildren): string
19 | (tag?: Tag, data?: VNodeData, children?: JSXStringChildren): string
20 | }
21 |
22 | function createJSX(tag?: Tag, children?: JSXStringChildren): string
23 | function createJSX(
24 | tag?: Tag,
25 | data?: VNodeData,
26 | children?: JSXStringChildren
27 | ): string
28 |
29 | /**
30 | * Custom renderer building JSX strings.
31 | */
32 | function createJSX(
33 | tag?: Tag,
34 | childrenOrData?: VNodeData | JSXStringChildren,
35 | _children?: JSXStringChildren
36 | ): string {
37 | const data = (childrenOrData instanceof Array ? {} : childrenOrData) || {}
38 | const children =
39 | (childrenOrData instanceof Array ? childrenOrData : _children) || []
40 |
41 | const tagName = getTagName(tag)
42 |
43 | const props = formatDataObject(data)
44 |
45 | return children.length
46 | ? `<${tagName}${props ? ' ' + props : ''}>${children.join('')}${tagName}>`
47 | : `<${tagName}${props ? ' ' + props : ''}/>`
48 | }
49 |
50 | export type JSXStringChildren = string[]
51 | export type Tag = string | AnyComponent | undefined
52 |
53 | /** Tag name for components that have no name on runtime */
54 | const Anonymous = 'Anonymous'
55 |
56 | /**
57 | * Get tag name from 1st argument for h (createJSX).
58 | * @param tag
59 | */
60 | const getTagName = (tag: Tag) => {
61 | if (!tag) {
62 | return Anonymous
63 | } else if (typeof tag === 'string') {
64 | return tag
65 | } else if (tag.name) {
66 | const t = tag as any
67 |
68 | if (!t.options) {
69 | return t.name || Anonymous
70 | } else {
71 | return t.options.name || Anonymous
72 | }
73 | } else {
74 | return Anonymous
75 | }
76 | }
77 |
78 | const formatDataObject = (dataObject: VNodeData): string =>
79 | [
80 | ...formatDataObjectItem(dataObject.props),
81 | ...formatDataObjectItem(dataObject.attrs),
82 | ...formatDataObjectItem(dataObject.domProps, 'domProps'),
83 | ...formatDataObjectItem(dataObject.on, 'on'),
84 | ...formatDataObjectItem(dataObject.nativeOn, 'nativeOn'),
85 | ...(dataObject.class ? [formatProp('class', dataObject.class)] : []),
86 | ...(dataObject.style ? [formatProp('style', dataObject.style)] : []),
87 | ...(dataObject.key ? [formatProp('key', dataObject.key)] : []),
88 | ...(dataObject.ref ? [formatProp('ref', dataObject.ref)] : []),
89 | ...(dataObject.slot ? [formatProp('slot', dataObject.slot)] : [])
90 | ].join(' ')
91 |
92 | const formatDataObjectItem = (
93 | item?: { [key: string]: any },
94 | prefix: string = ''
95 | ) =>
96 | item
97 | ? Object.keys(item).map(key =>
98 | formatProp(
99 | prefix ? prefix + key[0].toUpperCase() + key.slice(1) : key,
100 | item[key]
101 | )
102 | )
103 | : []
104 |
105 | const formatProp = (k: string, v: any): string =>
106 | typeof v === 'string'
107 | ? `${k}="${v}"`
108 | : typeof v === 'function'
109 | ? `${k}={${v.toString()}}`
110 | : `${k}={${JSON.stringify(v)}}`
111 |
--------------------------------------------------------------------------------
/src/utils/getTagNames.spec.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import { fromJSX, fromTemplate } from './getTagNames'
4 |
5 | describe('#fromTemplate', () => {
6 | it('Returns ["p"] for ', () => {
7 | expect(fromTemplate('')).toEqual(['p'])
8 | })
9 |
10 | it('Returns ["p"] for ', () => {
11 | expect(fromTemplate('')).toEqual(['p'])
12 | })
13 |
14 | it('Returns ["div","span","a"] for ', () => {
15 | expect(fromTemplate('')).toEqual([
16 | 'div',
17 | 'span',
18 | 'a'
19 | ])
20 | })
21 |
22 | it('Remove duplicate tags', () => {
23 | expect(fromTemplate('')).toEqual(['div'])
24 | })
25 |
26 | it('Works with multiline template', () => {
27 | expect(
28 | fromTemplate(`
29 |
32 | `)
33 | ).toEqual(['div', 'p'])
34 | })
35 |
36 | it('Keeps case of tag names', () => {
37 | expect(fromTemplate('')).toEqual(['FooBar'])
38 | })
39 | })
40 |
41 | describe('#fromJSX', () => {
42 | it('Returns ["p"] for h("p")', () => {
43 | expect(fromJSX(h => h('p'))).toEqual(['p'])
44 | })
45 |
46 | it('Returns ["span", "a", "p"] for h("p", [h("span"), h("a")])', () => {
47 | expect(fromJSX(h => h('p', [h('span'), h('a')]))).toEqual([
48 | 'span',
49 | 'a',
50 | 'p'
51 | ])
52 | })
53 |
54 | it('Parse component', () => {
55 | const Component = Vue.extend({
56 | render(h) {
57 | return h('div')
58 | }
59 | })
60 |
61 | expect(fromJSX(h => h(Component))).toEqual([])
62 | })
63 |
64 | it('Parse component', () => {
65 | const Component = Vue.component('foo', {
66 | render(h) {
67 | return h('div')
68 | }
69 | })
70 |
71 | expect(fromJSX(h => h(Component))).toEqual(['foo'])
72 | })
73 | })
74 |
--------------------------------------------------------------------------------
/src/utils/getTagNames.ts:
--------------------------------------------------------------------------------
1 | import dedent from 'dedent-tabs'
2 | import { VNodeChildren } from 'vue'
3 | import { ASTElement, compile } from 'vue-template-compiler'
4 |
5 | import removeDuplicates from './removeDuplicates'
6 |
7 | /**
8 | * Returns tags used in template.
9 | * @param template
10 | */
11 | export const fromTemplate = (template: string): string[] => {
12 | const { ast } = compile(dedent(template))
13 |
14 | if (!ast) {
15 | return []
16 | }
17 |
18 | return removeDuplicates(retrieveTagNamesFromAST(ast))
19 | }
20 |
21 | const retrieveTagNamesFromAST = (el: ASTElement): string[] => {
22 | return [
23 | ...Array.from(el.children || []).map(e => retrieveTagNamesFromAST(e))
24 | ].reduce((dest, cur) => [...dest, ...cur], el.tag ? [el.tag] : [])
25 | }
26 |
27 | type Render = (
28 | h: (tag: any, dataObject?: any, children?: VNodeChildren) => any,
29 | hack: any
30 | ) => any
31 |
32 | /**
33 | * Returns outermost tag name in JSX.
34 | * @param render
35 | */
36 | export const fromJSX = (render: Render): string[] => {
37 | const result: string[] = []
38 |
39 | render((tag, dataObject, _children) => {
40 | switch (typeof tag) {
41 | case 'string':
42 | result.push(tag)
43 | return
44 | case 'object':
45 | if (tag.name) {
46 | result.push(tag.name)
47 | return
48 | }
49 |
50 | return
51 | case 'function':
52 | // component created with Vue.component/Vue.extend
53 | if (tag.options && typeof tag.options.name === 'string') {
54 | result.push(tag.options.name)
55 | return
56 | }
57 |
58 | return
59 | default:
60 | console.warn(tag)
61 | return
62 | }
63 | }, {})
64 |
65 | return removeDuplicates(result)
66 | }
67 |
--------------------------------------------------------------------------------
/src/utils/removeDuplicates.spec.ts:
--------------------------------------------------------------------------------
1 | import removeDuplicates from './removeDuplicates'
2 |
3 | it('Remove duplicates', () => {
4 | expect(removeDuplicates([1, 2, 3, '1', 'bar', 3, 'foo', 'foo'])).toEqual([
5 | 1,
6 | 2,
7 | 3,
8 | '1',
9 | 'bar',
10 | 'foo'
11 | ])
12 | })
13 |
--------------------------------------------------------------------------------
/src/utils/removeDuplicates.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Remove duplicates from given array.
3 | */
4 | export default (arr: P[]): P[] => {
5 | return arr.filter((e, i) => arr.indexOf(e) === i)
6 | }
7 |
--------------------------------------------------------------------------------
/src/view/index.ts:
--------------------------------------------------------------------------------
1 | import addons from '@storybook/addons'
2 |
3 | import { Events } from '../addon'
4 | import { InfoAddonOptions } from '../options'
5 | import { StoryInfo } from '../types/info'
6 |
7 | export function renderToPanel(
8 | info: StoryInfo,
9 | options: InfoAddonOptions
10 | ): void {
11 | const channel = addons.getChannel()
12 |
13 | channel.emit(Events.ShowDocs, { info, options })
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./lib",
4 | "lib": ["dom", "es5", "es2015", "es2015.promise"],
5 | "jsx": "react",
6 | "target": "es5",
7 | "strict": true,
8 | "module": "es2015",
9 | "moduleResolution": "node",
10 | "removeComments": true,
11 | "emitDecoratorMetadata": true,
12 | "experimentalDecorators": true,
13 | "allowSyntheticDefaultImports": true,
14 | "esModuleInterop": true,
15 | "strictNullChecks": true,
16 | "declaration": true
17 | },
18 | "include": ["src/**/*.ts", "src/**/*.d.ts"],
19 | "exclude": ["node_modules", "lib"]
20 | }
21 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint:latest",
4 | "tslint-config-prettier"
5 | ],
6 | "rules": {
7 | "no-console": false,
8 | "interface-name": [
9 | true,
10 | "never-prefix"
11 | ],
12 | "object-literal-sort-keys": false,
13 | "variable-name": [
14 | true,
15 | "check-format",
16 | "allow-leading-underscore",
17 | "allow-pascal-case"
18 | ],
19 | "unified-signatures": false
20 | },
21 | "linterOptions": {
22 | "exclude": [
23 | "**/*.spec.ts"
24 | ]
25 | }
26 | }
--------------------------------------------------------------------------------