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"