├── .gitignore ├── .vscode └── settings.json ├── src ├── index.js ├── loader.js ├── util.js └── register.js ├── package.json ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .cache 4 | dist 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import storyLoader from "./loader"; 2 | import registerStories from "./register"; 3 | 4 | export { registerStories, storyLoader }; 5 | export default storyLoader; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-storybook", 3 | "version": "1.2.0", 4 | "description": "Custom block for your Single File Components (SFC)", 5 | "main": "dist/index.js", 6 | "author": "Matt Rothenberg", 7 | "license": "ISC", 8 | "scripts": { 9 | "build": "NODE_ENV=production parcel build src/index.js", 10 | "dev": "parcel watch src/index.js", 11 | "prepublishOnly": "npm run build" 12 | }, 13 | "dependencies": { 14 | "serialize-javascript": "^2.1.1", 15 | "webpack-parse-query": "^1.0.1" 16 | }, 17 | "devDependencies": { 18 | "husky": "^1.3.1", 19 | "parcel-bundler": "^1.12.3", 20 | "parcel-plugin-bundle-visualiser": "^1.2.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/loader.js: -------------------------------------------------------------------------------- 1 | import parseQuery from "webpack-parse-query"; 2 | import serialize from "serialize-javascript"; 3 | 4 | function generateCode(source, ctx) { 5 | let code = ""; 6 | const query = parseQuery(ctx.resourceQuery); 7 | const story = { 8 | template: source.trim(), 9 | name: query.name, 10 | group: query.group, 11 | methods: query.methods, 12 | notes: query.notes, 13 | knobs: query.knobs, 14 | options: query.options, 15 | parameters: query.parameters 16 | }; 17 | 18 | code += `function (Component) { 19 | Component.options.__stories = Component.options.__stories || [] 20 | Component.options.__stories.push(${serialize(story)}) 21 | }\n`; 22 | return code; 23 | } 24 | 25 | export default function loader(source) { 26 | const story = generateCode(source, this); 27 | this.callback(null, `module.exports = ${story}`); 28 | } 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Vue Storybook Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## 0.4.0 8 | 9 | - Expose `group` attribute for grouping related stories. 10 | 11 | ```html 12 | 15 | ... 16 | 17 | ``` 18 | 19 | ## 0.3.0 20 | 21 | - Breaking API change around story registration. 22 | 23 | - As per https://github.com/mattrothenberg/vue-storybook/issues/3, stories are now automatically registered by the very presence of tags in a given component. 24 | 25 | ```js 26 | import { registerStories } from 'vue-storybook' 27 | import { configure } from '@storybook/vue'; 28 | 29 | const req = require.context('./', true, /\.vue$/) 30 | function loadStories() { 31 | req.keys().forEach((filename) => { 32 | registerStories(req, filename, storiesOf, {}) 33 | }) 34 | } 35 | loadStories() 36 | ``` 37 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | function upperFirst(str) { 2 | return str.charAt(0).toUpperCase() + str.slice(1); 3 | } 4 | 5 | function camelCase(str) { 6 | let s = 7 | str && 8 | str 9 | .match( 10 | /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g 11 | ) 12 | .map(x => x.slice(0, 1).toUpperCase() + x.slice(1).toLowerCase()) 13 | .join(""); 14 | return s.slice(0, 1).toLowerCase() + s.slice(1); 15 | } 16 | 17 | function parseKnobsObject(obj, plugins) { 18 | return Function( 19 | `return ({ text, boolean, number, select, color, radios, date, files, object, array, optionsKnob, button }) => (${obj})` 20 | )()(plugins); 21 | } 22 | 23 | function looseJsonParse(obj) { 24 | return Function('"use strict";return (' + obj + ")")(); 25 | } 26 | 27 | function getComponentNameFromFilename(fileName) { 28 | return upperFirst( 29 | camelCase(fileName.replace(/^.+\/[\W_]*?/, "").replace(/\.\w+$/, "")) 30 | ); 31 | } 32 | 33 | export { 34 | camelCase, 35 | upperFirst, 36 | parseKnobsObject, 37 | getComponentNameFromFilename, 38 | looseJsonParse 39 | }; 40 | -------------------------------------------------------------------------------- /src/register.js: -------------------------------------------------------------------------------- 1 | import { 2 | upperFirst, 3 | camelCase, 4 | parseKnobsObject, 5 | looseJsonParse, 6 | getComponentNameFromFilename 7 | } from "./util"; 8 | 9 | const initialConfig = { 10 | knobs: {}, 11 | decorators: [], 12 | methods: {} 13 | }; 14 | 15 | function buildStory({ story, component, name, storiesOf }, config) { 16 | const { methods, knobs, decorators } = config; 17 | const storiesOfInstance = storiesOf(story.group || "vue-storybook", module); 18 | const componentFunc = () => { 19 | let data = story.knobs ? parseKnobsObject(story.knobs, knobs) : {}; 20 | return { 21 | components: { 22 | [name]: component.default || component 23 | }, 24 | props: data, 25 | template: story.template, 26 | methods: { 27 | action(name, ...payload) { 28 | if (methods.action) { 29 | methods.action(name)(...payload); 30 | } else { 31 | console.warn("You forgot to add the action method!"); 32 | } 33 | } 34 | } 35 | }; 36 | }; 37 | 38 | if (decorators) { 39 | decorators.forEach(storiesOfInstance.addDecorator); 40 | } 41 | 42 | storiesOfInstance.add(story.name, componentFunc, { 43 | notes: story.notes, 44 | ...(story.options ? { options: looseJsonParse(story.options) } : null), 45 | ...(story.parameters ? looseJsonParse(story.parameters) : null) 46 | }); 47 | } 48 | 49 | export default function registerStories( 50 | req, 51 | fileName, 52 | storiesOf, 53 | config = initialConfig 54 | ) { 55 | const component = req(fileName); 56 | const name = getComponentNameFromFilename(fileName); 57 | 58 | const stories = 59 | component.__stories || 60 | component.default.__stories || 61 | (component.default.options || {}).__stories; 62 | 63 | if (!stories) return; 64 | stories.forEach(story => 65 | buildStory({ story, name, component, storiesOf }, config) 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-storybook 2 | 3 | Custom `` blocks for Vue single file components that work out of the box with Vue CLI 3 and [`vue-cli-plugin-storybook`](https://github.com/storybooks/vue-cli-plugin-storybook). 4 | 5 | [![npm version](https://badge.fury.io/js/vue-storybook.svg)](https://badge.fury.io/js/vue-storybook) 6 | 7 | ```bash 8 | yarn add vue-storybook 9 | ``` 10 | 11 | ```js 12 | const { storyLoader, registerStories } = require("vue-storybook"); 13 | ``` 14 | 15 | ## What is this? 16 | 17 | A **Webpack loader** + **helper script** that allows you to embellish your pre-existing Vue single file components (SFC) with a custom `` block that's automatically translated into a [Storybook](https://github.com/storybooks/storybook)-flavored story. 18 | 19 | ### Hello World Example 20 | 21 | Repo: https://github.com/mattrothenberg/vue-storybook-example-project 22 | 23 | ```vue 24 | 25 | Can't touch this 26 | 27 | ``` 28 | 29 | turns into: 30 | 31 | ![screen shot 2018-03-04 at 10 43 54 am](https://user-images.githubusercontent.com/5148596/36947401-13794112-1f99-11e8-89d8-0741cc38ee45.png) 32 | 33 | ## How does it work? 34 | 35 | Given an existing Vue CLI + `vue-cli-plugin-storybook` project, modify your project's `vue.config.js` thusly. 36 | 37 | ```js 38 | // vue.config.js 39 | module.exports = { 40 | chainWebpack: config => { 41 | config.resolve.symlinks(false); 42 | }, 43 | configureWebpack: config => { 44 | config.module.rules.push({ 45 | resourceQuery: /blockType=story/, 46 | loader: "vue-storybook" 47 | }); 48 | } 49 | }; 50 | ``` 51 | 52 | Add a custom `` block to your single file component. The following Storybook plugins/APIs are supported: 53 | 54 | - Actions 55 | - Story Options 56 | - Notes 57 | - Knobs 58 | 59 | It is also possible to pass options for other addons using the `parameters` attribute. 60 | 61 | You can optionally group components by specifiying a `group` attribute. 62 | 63 | ```vue 64 | 83 | {{initialText}} 84 | 85 | ``` 86 | 87 | Then, in your main `index.stories.js` (or wherever your write your stories), leverage our helper script to start adding stories. NB: the signature of `registerStories` has changed significantly. 88 | 89 | ```js 90 | registerStories(req, filename, storiesOf, config); 91 | ``` 92 | 93 | `config` is now an object with the following keys, 94 | 95 | ```js 96 | { 97 | knobs: { 98 | // put the knobs you plan on using 99 | // (things like `text` or `select`) 100 | // in this object 101 | }, 102 | decorators: [ 103 | // an array of decorator functions 104 | ], 105 | methods: { 106 | action // where action is the exported member from `addon-actions` 107 | } 108 | } 109 | ``` 110 | 111 | ```js 112 | // Import Storybook + all 'yr plugins! 113 | import { storiesOf } from "@storybook/vue"; 114 | import { action } from "@storybook/addon-actions"; 115 | import { withNotes } from "@storybook/addon-notes"; 116 | import { withKnobs, text, boolean } from "@storybook/addon-knobs/vue"; 117 | 118 | // Import our helper 119 | import { registerStories } from "vue-storybook"; 120 | 121 | // Require the Vue SFC with blocks inside 122 | const req = require.context("./", true, /\.vue$/); 123 | 124 | // Programatically register these stories 125 | function loadStories() { 126 | req.keys().forEach(filename => { 127 | let config = { 128 | knobs: { 129 | text, 130 | boolean 131 | }, 132 | decorators: [ 133 | withKnobs, 134 | function(context, story) { 135 | return { 136 | template: ` 137 |
` 138 | }; 139 | } 140 | ], 141 | methods: { 142 | action 143 | } 144 | }; 145 | 146 | registerStories(req, filename, storiesOf, config); 147 | }); 148 | } 149 | 150 | // Let's go! 151 | configure(loadStories, module) 152 | ``` 153 | 154 | ## Roadmap 155 | 156 | - [x] Actions 157 | - [x] Knobs 158 | - [x] Notes 159 | - [ ] Info 160 | - [ ] Readme 161 | --------------------------------------------------------------------------------