diff --git a/ADDONS_SUPPORT.md b/ADDONS_SUPPORT.md index e28ec790ab7b..4b56c6ae8dc7 100644 --- a/ADDONS_SUPPORT.md +++ b/ADDONS_SUPPORT.md @@ -2,16 +2,17 @@ | |[React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)| | ----------- |:-------:|:-------:|:-------:|:-------:|:-------:| -|[a11y](addons/a11y) |+| | | | | -|[actions](addons/actions) |+|+|+|+|+| -|[background](addons/background)|+| | | | | -|[centered](addons/centered) |+| |+| | | -|[events](addons/events) |+| | | | | -|[graphql](addons/graphql) |+| | | | | -|[info](addons/info) |+| | | | | -|[jest](addons/jest) |+| | | | | -|[knobs](addons/knobs) |+|+|+|+|+| -|[links](addons/links) |+|+|+|+| | -|[notes](addons/notes) |+| |+|+|+| -|[storyshots](addons/storyshots)|+|+|+|+| | -|[viewport](addons/viewport) |+| | | | | +|[a11y](addons/a11y) |+| | | | | +|[actions](addons/actions) |+|+|+|+|+| +|[background](addons/background) |+| | | | | +|[centered](addons/centered) |+| |+| | | +|[events](addons/events) |+| | | | | +|[graphql](addons/graphql) |+| | | | | +|[info](addons/info) |+| | | | | +|[jest](addons/jest) |+| | | | | +|[knobs](addons/knobs) |+|+|+|+|+| +|[links](addons/links) |+|+|+|+| | +|[notes](addons/notes) |+| |+|+|+| +|[storyshots](addons/storyshots) |+|+|+|+| | +|[storysource](addons/storysource)|+| |+|+|+| +|[viewport](addons/viewport) |+| | | | | diff --git a/__mocks__/inject-decorator.ugly-comments-stories.txt b/__mocks__/inject-decorator.ugly-comments-stories.txt new file mode 100644 index 000000000000..926e8cd5e97a --- /dev/null +++ b/__mocks__/inject-decorator.ugly-comments-stories.txt @@ -0,0 +1,23 @@ +/* global window */ +/* eslint-disable global-require, import/no-dynamic-require */ + +import React from 'react'; + +/* + eslint-disable some kind + of multi line ignore, though + I'm not sure it's possible. +*/ + +import { storiesOf } from '@storybook/react'; + +/* eslint-disable-line */ const x = 0; + +// eslint-disable-line +storiesOf('Foo', module) + .add('bar', () =>
baz
); + +/* + This is actually a good comment that will help + users to understand what's going on here. +*/ \ No newline at end of file diff --git a/addons/storysource/README.md b/addons/storysource/README.md index 5e792c2ef497..57226d05204b 100644 --- a/addons/storysource/README.md +++ b/addons/storysource/README.md @@ -2,6 +2,8 @@ This addon is used to show stories source in the addon panel. +![Storysource Demo](demo.gif) + ## Getting Started First, install the addon @@ -31,3 +33,83 @@ module.exports = { }, }; ``` + +## Loader Options + +The loader can be customized with the following options: + +### prettierConfig + +The prettier configuration that will be used to format the story source in the addon panel. + +Defaults: +```js +{ + printWidth: 120, + tabWidth: 2, + bracketSpacing: true, + trailingComma: 'es5', + singleQuote: true, +} +``` + +Usage: + +```js +module.exports = { + module: { + rules: [ + { + test: /\.stories\.jsx?$/, + loaders: [ + { + loader: require.resolve('@storybook/addon-storysource/loader'), + options: { + prettierConfig: { + printWidth: 80, + singleQuote: false, + } + } + } + ], + enforce: 'pre', + }, + ], + }, +}; +``` + +### uglyCommentsRegex + +The array of regex that is used to remove "ugly" comments. + +Defaults: +```js +[/^eslint-.*/, /^global.*/] +``` + +Usage: + +```js +module.exports = { + module: { + rules: [ + { + test: /\.stories\.jsx?$/, + loaders: [ + { + loader: require.resolve('@storybook/addon-storysource/loader'), + options: { + uglyCommentsRegex: [ + /^eslint-.*/, + /^global.*/, + ] + } + } + ], + enforce: 'pre', + }, + ], + }, +}; +``` diff --git a/addons/storysource/demo.gif b/addons/storysource/demo.gif new file mode 100644 index 000000000000..34077ebb939e Binary files /dev/null and b/addons/storysource/demo.gif differ diff --git a/addons/storysource/package.json b/addons/storysource/package.json index ebb0f53d5cfe..4c48f5823d7f 100644 --- a/addons/storysource/package.json +++ b/addons/storysource/package.json @@ -25,7 +25,8 @@ "acorn-jsx": "^4.1.1", "acorn-stage3": "^0.5.0", "estraverse": "^4.2.0", - "line-column": "^1.0.2", + "loader-utils": "^1.1.0", + "prettier": "^1.10.2", "prop-types": "^15.5.10", "react-syntax-highlighter": "^7.0.0" }, diff --git a/addons/storysource/src/loader/__snapshots__/inject-decorator.test.js.snap b/addons/storysource/src/loader/__snapshots__/inject-decorator.test.js.snap index 065b5599ec2c..ac8cc9de27cb 100644 --- a/addons/storysource/src/loader/__snapshots__/inject-decorator.test.js.snap +++ b/addons/storysource/src/loader/__snapshots__/inject-decorator.test.js.snap @@ -4,11 +4,11 @@ exports[`inject-decorator positive - angular calculates "adds" map 1`] = ` Object { "Custom|ng-content@Default": Object { "endLoc": Object { - "col": 3, + "col": 2, "line": 15, }, "startLoc": Object { - "col": 44, + "col": 43, "line": 10, }, }, @@ -38,141 +38,141 @@ exports[`inject-decorator positive calculates "adds" map 1`] = ` Object { "Addons|Info.Decorator@Use Info as story decorator": Object { "endLoc": Object { - "col": 74, + "col": 73, "line": 137, }, "startLoc": Object { - "col": 8, + "col": 7, "line": 137, }, }, "Addons|Info.GitHub issues@#1814": Object { "endLoc": Object { - "col": 5, + "col": 4, "line": 152, }, "startLoc": Object { - "col": 3, + "col": 2, "line": 146, }, }, "Addons|Info.Markdown@Displays Markdown in description": Object { "endLoc": Object { - "col": 97, + "col": 96, "line": 44, }, "startLoc": Object { - "col": 3, + "col": 2, "line": 43, }, }, "Addons|Info.Options.TableComponent@Use a custom component for the table": Object { "endLoc": Object { - "col": 42, + "col": 41, "line": 130, }, "startLoc": Object { - "col": 3, + "col": 2, "line": 127, }, }, "Addons|Info.Options.header@Shows or hides Info Addon header": Object { "endLoc": Object { - "col": 42, + "col": 41, "line": 60, }, "startLoc": Object { - "col": 3, + "col": 2, "line": 56, }, }, "Addons|Info.Options.inline@Inlines component inside story": Object { "endLoc": Object { - "col": 42, + "col": 41, "line": 52, }, "startLoc": Object { - "col": 3, + "col": 2, "line": 48, }, }, "Addons|Info.Options.propTables@Shows additional component prop tables": Object { "endLoc": Object { - "col": 42, + "col": 41, "line": 76, }, "startLoc": Object { - "col": 3, + "col": 2, "line": 72, }, }, "Addons|Info.Options.propTablesExclude@Exclude component from prop tables": Object { "endLoc": Object { - "col": 5, + "col": 4, "line": 89, }, "startLoc": Object { - "col": 3, + "col": 2, "line": 80, }, }, "Addons|Info.Options.source@Shows or hides Info Addon source": Object { "endLoc": Object { - "col": 42, + "col": 41, "line": 68, }, "startLoc": Object { - "col": 3, + "col": 2, "line": 64, }, }, "Addons|Info.Options.styles@Extend info styles with an object": Object { "endLoc": Object { - "col": 44, + "col": 43, "line": 108, }, "startLoc": Object { - "col": 5, + "col": 4, "line": 94, }, }, "Addons|Info.Options.styles@Full control over styles using a function": Object { "endLoc": Object { - "col": 44, + "col": 43, "line": 123, }, "startLoc": Object { - "col": 5, + "col": 4, "line": 111, }, }, "Addons|Info.React Docgen@Comments from Flow declarations": Object { "endLoc": Object { - "col": 86, + "col": 85, "line": 22, }, "startLoc": Object { - "col": 5, + "col": 4, "line": 19, }, }, "Addons|Info.React Docgen@Comments from PropType declarations": Object { "endLoc": Object { - "col": 80, + "col": 79, "line": 16, }, "startLoc": Object { - "col": 5, + "col": 4, "line": 13, }, }, "Addons|Info.React Docgen@Comments from component declaration": Object { "endLoc": Object { - "col": 71, + "col": 70, "line": 28, }, "startLoc": Object { - "col": 5, + "col": 4, "line": 25, }, }, @@ -336,6 +336,22 @@ storiesOf('Addons|Info.GitHub issues', module).addDecorator(withStorySource(__ST " `; +exports[`inject-decorator stories with ugly comments should delete ugly comments from the generated story source 1`] = ` +"import React from 'react'; + +import { storiesOf } from '@storybook/react'; + +const x = 0; + +storiesOf('Foo', module).add('bar', () =>
baz
); + +/* + This is actually a good comment that will help + users to understand what's going on here. +*/ +" +`; + exports[`inject-decorator will not change the source when there are no "storiesOf" functions 1`] = ` "while(true) { console.log(\\"it's a kind of magic\\"); diff --git a/addons/storysource/src/loader/default-options.js b/addons/storysource/src/loader/default-options.js new file mode 100644 index 000000000000..97a58bd808dc --- /dev/null +++ b/addons/storysource/src/loader/default-options.js @@ -0,0 +1,12 @@ +const defaultOptions = { + prettierConfig: { + printWidth: 120, + tabWidth: 2, + bracketSpacing: true, + trailingComma: 'es5', + singleQuote: true, + }, + uglyCommentsRegex: [/^eslint-.*/, /^global.*/], +}; + +export default defaultOptions; diff --git a/addons/storysource/src/loader/generate-helpers.js b/addons/storysource/src/loader/generate-helpers.js new file mode 100644 index 000000000000..4ad25a2e91d3 --- /dev/null +++ b/addons/storysource/src/loader/generate-helpers.js @@ -0,0 +1,104 @@ +import prettier from 'prettier'; +import { handleADD, handleSTORYOF } from './parse-helpers'; + +const estraverse = require('estraverse'); +const acorn = require('acorn'); + +require('acorn-stage3/inject')(acorn); +require('acorn-jsx/inject')(acorn); +require('acorn-es7')(acorn); + +const acornConfig = { + ecmaVersion: '9', + sourceType: 'module', + ranges: true, + locations: true, + plugins: { + jsx: true, + stage3: true, + es7: true, + }, +}; + +function isUglyComment(comment, uglyCommentsRegex) { + return uglyCommentsRegex.some(regex => regex.test(comment)); +} + +function generateSourceWithoutUglyComments(source, { comments, uglyCommentsRegex }) { + let lastIndex = 0; + const parts = [source]; + + comments + .filter(comment => isUglyComment(comment.value.trim(), uglyCommentsRegex)) + .forEach(comment => { + parts.pop(); + + const start = source.slice(lastIndex, comment.start); + const end = source.slice(comment.end); + + parts.push(start, end); + lastIndex = comment.end; + }); + + return parts.join(''); +} + +function prettifyCode(source, { prettierConfig }) { + return prettier.format(source, prettierConfig); +} + +export function generateSourceWithDecorators(source, decorator) { + const comments = []; + + const config = { + ...acornConfig, + onComment: comments, + }; + + const ast = acorn.parse(source, config); + + let lastIndex = 0; + const parts = [source]; + + estraverse.traverse(ast, { + fallback: 'iteration', + enter: node => { + if (node.type === 'CallExpression') { + lastIndex = handleSTORYOF(node, parts, source, lastIndex); + } + }, + }); + + const newSource = parts.join(decorator); + + return { + changed: lastIndex > 0, + source: newSource, + comments, + }; +} + +export function generateAddsMap(source) { + const ast = acorn.parse(source, acornConfig); + const adds = {}; + + estraverse.traverse(ast, { + fallback: 'iteration', + enter: (node, parent) => { + if (node.type === 'MemberExpression') { + handleADD(node, parent, adds); + } + }, + }); + + return adds; +} + +export function generateStorySource({ source, ...options }) { + let storySource = source; + + storySource = generateSourceWithoutUglyComments(storySource, options); + storySource = prettifyCode(storySource, options); + + return storySource; +} diff --git a/addons/storysource/src/loader/index.js b/addons/storysource/src/loader/index.js index cb7ac63d2810..23cf57820af8 100644 --- a/addons/storysource/src/loader/index.js +++ b/addons/storysource/src/loader/index.js @@ -1,15 +1,17 @@ +import { getOptions } from 'loader-utils'; import injectDecorator from './inject-decorator'; const ADD_DECORATOR_STATEMENT = '.addDecorator(withStorySource(__STORY__, __ADDS_MAP__))'; function transform(source) { - const result = injectDecorator(source, ADD_DECORATOR_STATEMENT); + const options = getOptions(this) || {}; + const result = injectDecorator(source, ADD_DECORATOR_STATEMENT, options); if (!result.changed) { return source; } - const sourceJson = JSON.stringify(source) + const sourceJson = JSON.stringify(result.storySource) .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029'); diff --git a/addons/storysource/src/loader/inject-decorator.js b/addons/storysource/src/loader/inject-decorator.js index 5520dafedeed..6a83617013fc 100644 --- a/addons/storysource/src/loader/inject-decorator.js +++ b/addons/storysource/src/loader/inject-decorator.js @@ -1,72 +1,39 @@ -import lineColumn from 'line-column'; -import { handleADD, handleSTORYOF } from './parse-helpers'; +import defaultOptions from './default-options'; -const estraverse = require('estraverse'); -const acorn = require('acorn'); +import { + generateSourceWithDecorators, + generateStorySource, + generateAddsMap, +} from './generate-helpers'; -require('acorn-stage3/inject')(acorn); -require('acorn-jsx/inject')(acorn); -require('acorn-es7')(acorn); - -const acornConfig = { - ecmaVersion: '9', - sourceType: 'module', - plugins: { - jsx: true, - stage3: true, - es7: true, - }, -}; - -function calculateLocations(source, adds) { - const addsKeys = Object.keys(adds); - - if (!addsKeys.length) { - return {}; - } - - const lineColumnFinder = lineColumn(source); - - return addsKeys.reduce((map, key) => { - const value = adds[key]; - - // eslint-disable-next-line no-param-reassign - map[key] = { - startLoc: lineColumnFinder.fromIndex(value.start), - endLoc: lineColumnFinder.fromIndex(value.end), - }; - - return map; - }, {}); +function extendOptions(source, comments, options) { + return { + ...defaultOptions, + ...options, + source, + comments, + }; } -function inject(source, decorator) { - const ast = acorn.parse(source, acornConfig); - - let lastIndex = 0; - const parts = [source]; - const adds = {}; +function inject(source, decorator, options = {}) { + const { changed, source: newSource, comments } = generateSourceWithDecorators(source, decorator); - estraverse.traverse(ast, { - fallback: 'iteration', - enter: (node, parent) => { - if (node.type === 'MemberExpression') { - handleADD(node, parent, adds); - } - - if (node.type === 'CallExpression') { - lastIndex = handleSTORYOF(node, parts, source, lastIndex); - } - }, - }); + if (!changed) { + return { + source: newSource, + addsMap: {}, + changed, + }; + } - const addsMap = calculateLocations(source, adds); - const newSource = parts.join(decorator); + const storySource = generateStorySource(extendOptions(source, comments, options)); + const addsMap = generateAddsMap(storySource); return { - changed: lastIndex > 0, source: newSource, + storySource, addsMap, + changed, }; } diff --git a/addons/storysource/src/loader/inject-decorator.test.js b/addons/storysource/src/loader/inject-decorator.test.js index 5fe68ddb8d02..18bd8a1acdac 100644 --- a/addons/storysource/src/loader/inject-decorator.test.js +++ b/addons/storysource/src/loader/inject-decorator.test.js @@ -38,6 +38,18 @@ describe('inject-decorator', () => { }); }); + describe('stories with ugly comments', () => { + const source = fs.readFileSync( + './__mocks__/inject-decorator.ugly-comments-stories.txt', + 'utf-8' + ); + const result = injectDecorator(source, ADD_DECORATOR_STATEMENT); + + it('should delete ugly comments from the generated story source', () => { + expect(result.storySource).toMatchSnapshot(); + }); + }); + it('will not change the source when there are no "storiesOf" functions', () => { const source = fs.readFileSync('./__mocks__/inject-decorator.no-stories.txt', 'utf-8'); diff --git a/addons/storysource/src/loader/parse-helpers.js b/addons/storysource/src/loader/parse-helpers.js index d89764f5f1c7..09bf13f1f255 100644 --- a/addons/storysource/src/loader/parse-helpers.js +++ b/addons/storysource/src/loader/parse-helpers.js @@ -66,8 +66,14 @@ export function handleADD(node, parent, adds) { // eslint-disable-next-line no-param-reassign adds[key] = { // Debug: code: source.slice(storyName.start, lastArg.end), - start: storyName.start, - end: lastArg.end, + startLoc: { + col: storyName.loc.start.column, + line: storyName.loc.start.line, + }, + endLoc: { + col: lastArg.loc.end.column, + line: lastArg.loc.end.line, + }, }; } diff --git a/addons/storysource/src/preview.js b/addons/storysource/src/preview.js index 76ecc883effc..96c38ccd3358 100644 --- a/addons/storysource/src/preview.js +++ b/addons/storysource/src/preview.js @@ -1,9 +1,13 @@ import addons from '@storybook/addons'; import { EVENT_ID } from './'; -function setStorySource(source, map, context) { +function getLocation(context, locationsMap) { + return locationsMap[`${context.kind}@${context.story}`] || locationsMap[`@${context.story}`]; +} + +function setStorySource(context, source, locationsMap) { const channel = addons.getChannel(); - const location = map[`${context.kind}@${context.story}`] || map[`@${context.story}`]; + const location = getLocation(context, locationsMap); channel.emit(EVENT_ID, { source, @@ -11,9 +15,9 @@ function setStorySource(source, map, context) { }); } -export function withStorySource(source, map) { +export function withStorySource(source, locationsMap = {}) { return (story, context) => { - setStorySource(source, map, context); + setStorySource(context, source, locationsMap); return story(); }; } diff --git a/yarn.lock b/yarn.lock index 1686f69be1f0..f243fb2cc601 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9316,13 +9316,6 @@ lie@~3.1.0: dependencies: immediate "~3.0.5" -line-column@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/line-column/-/line-column-1.0.2.tgz#d25af2936b6f4849172b312e4792d1d987bc34a2" - dependencies: - isarray "^1.0.0" - isobject "^2.0.0" - linkify-it@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f"