diff --git a/.babelrc.js b/.babelrc.js
index 3fa92abb65dd..b60cfbe892d7 100644
--- a/.babelrc.js
+++ b/.babelrc.js
@@ -54,6 +54,7 @@ module.exports = {
},
{
test: './lib',
+ exclude: './addons/storysource/src/loader',
presets: [
['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage', corejs: '3' }],
'@babel/preset-react',
@@ -77,6 +78,7 @@ module.exports = {
'./lib/core/src/server',
'./lib/node-logger',
'./lib/codemod',
+ './lib/source-loader/src',
'./addons/storyshots',
'./addons/storysource/src/loader',
'./app/**/src/server/**',
diff --git a/examples/angular-cli/.storybook/webpack.config.ts b/examples/angular-cli/.storybook/webpack.config.ts
index 3a2d1f390e3f..3a8badcaf3a1 100644
--- a/examples/angular-cli/.storybook/webpack.config.ts
+++ b/examples/angular-cli/.storybook/webpack.config.ts
@@ -5,7 +5,7 @@ module.exports = async ({ config }: { config: any }) => {
test: [/\.stories\.tsx?$/, /index\.ts$/],
loaders: [
{
- loader: require.resolve('@storybook/addon-storysource/loader'),
+ loader: require.resolve('@storybook/source-loader'),
options: {
parser: 'typescript',
},
diff --git a/examples/ember-cli/.storybook/webpack.config.js b/examples/ember-cli/.storybook/webpack.config.js
index 915eb22693e8..d382a097d811 100644
--- a/examples/ember-cli/.storybook/webpack.config.js
+++ b/examples/ember-cli/.storybook/webpack.config.js
@@ -3,7 +3,7 @@ const path = require('path');
module.exports = async ({ config }) => {
config.module.rules.push({
test: [/\.stories\.js$/, /index\.js$/],
- loaders: [require.resolve('@storybook/addon-storysource/loader')],
+ loaders: [require.resolve('@storybook/source-loader')],
include: [path.resolve(__dirname, '../')],
enforce: 'pre',
});
diff --git a/examples/ember-cli/package.json b/examples/ember-cli/package.json
index 6425b7f90722..6df3e90e628a 100644
--- a/examples/ember-cli/package.json
+++ b/examples/ember-cli/package.json
@@ -27,6 +27,7 @@
"@storybook/addon-viewport": "5.2.0-alpha.23",
"@storybook/addons": "5.2.0-alpha.23",
"@storybook/ember": "5.2.0-alpha.23",
+ "@storybook/source-loader": "5.2.0-alpha.23",
"babel-loader": "^8",
"broccoli-asset-rev": "^3.0.0",
"cross-env": "^5.2.0",
diff --git a/examples/html-kitchen-sink/.storybook/webpack.config.js b/examples/html-kitchen-sink/.storybook/webpack.config.js
index d2d694457e04..49d6279ea521 100644
--- a/examples/html-kitchen-sink/.storybook/webpack.config.js
+++ b/examples/html-kitchen-sink/.storybook/webpack.config.js
@@ -3,7 +3,7 @@ const path = require('path');
module.exports = async ({ config }) => {
config.module.rules.push({
test: [/\.stories\.js$/, /index\.js$/],
- loaders: [require.resolve('@storybook/addon-storysource/loader')],
+ loaders: [require.resolve('@storybook/source-loader')],
include: [path.resolve(__dirname, '../stories')],
enforce: 'pre',
});
diff --git a/examples/html-kitchen-sink/package.json b/examples/html-kitchen-sink/package.json
index ff3ce8a491df..e41d18dc8b9e 100644
--- a/examples/html-kitchen-sink/package.json
+++ b/examples/html-kitchen-sink/package.json
@@ -31,6 +31,7 @@
"@storybook/core": "5.2.0-alpha.23",
"@storybook/core-events": "5.2.0-alpha.23",
"@storybook/html": "5.2.0-alpha.23",
+ "@storybook/source-loader": "5.2.0-alpha.23",
"eventemitter3": "^3.1.0",
"format-json": "^1.0.3",
"global": "^4.3.2"
diff --git a/examples/marko-cli/.storybook/webpack.config.js b/examples/marko-cli/.storybook/webpack.config.js
index 8c62bf31bd42..5c4768c30ce1 100644
--- a/examples/marko-cli/.storybook/webpack.config.js
+++ b/examples/marko-cli/.storybook/webpack.config.js
@@ -3,7 +3,7 @@ const path = require('path');
module.exports = async ({ config }) => {
config.module.rules.push({
test: [/\.stories\.js$/],
- loaders: [require.resolve('@storybook/addon-storysource/loader')],
+ loaders: [require.resolve('@storybook/source-loader')],
include: [path.resolve(__dirname, '../src')],
enforce: 'pre',
});
diff --git a/examples/marko-cli/package.json b/examples/marko-cli/package.json
index 57d3538ab5ca..9adedcf735cb 100644
--- a/examples/marko-cli/package.json
+++ b/examples/marko-cli/package.json
@@ -31,6 +31,7 @@
"@storybook/addon-storysource": "5.2.0-alpha.23",
"@storybook/addons": "5.2.0-alpha.23",
"@storybook/marko": "5.2.0-alpha.23",
+ "@storybook/source-loader": "5.2.0-alpha.23",
"prettier": "^1.16.4",
"webpack": "^4.33.0"
}
diff --git a/examples/mithril-kitchen-sink/.storybook/webpack.config.js b/examples/mithril-kitchen-sink/.storybook/webpack.config.js
index 8c62bf31bd42..5c4768c30ce1 100644
--- a/examples/mithril-kitchen-sink/.storybook/webpack.config.js
+++ b/examples/mithril-kitchen-sink/.storybook/webpack.config.js
@@ -3,7 +3,7 @@ const path = require('path');
module.exports = async ({ config }) => {
config.module.rules.push({
test: [/\.stories\.js$/],
- loaders: [require.resolve('@storybook/addon-storysource/loader')],
+ loaders: [require.resolve('@storybook/source-loader')],
include: [path.resolve(__dirname, '../src')],
enforce: 'pre',
});
diff --git a/examples/mithril-kitchen-sink/package.json b/examples/mithril-kitchen-sink/package.json
index ea7802446a95..73e02448770d 100644
--- a/examples/mithril-kitchen-sink/package.json
+++ b/examples/mithril-kitchen-sink/package.json
@@ -24,6 +24,7 @@
"@storybook/addon-viewport": "5.2.0-alpha.23",
"@storybook/addons": "5.2.0-alpha.23",
"@storybook/mithril": "5.2.0-alpha.23",
+ "@storybook/source-loader": "5.2.0-alpha.23",
"webpack": "^4.33.0"
}
}
diff --git a/examples/official-storybook/package.json b/examples/official-storybook/package.json
index 68da05717790..96af26fd720d 100644
--- a/examples/official-storybook/package.json
+++ b/examples/official-storybook/package.json
@@ -41,6 +41,7 @@
"@storybook/core-events": "5.2.0-alpha.23",
"@storybook/node-logger": "5.2.0-alpha.23",
"@storybook/react": "5.2.0-alpha.23",
+ "@storybook/source-loader": "5.2.0-alpha.23",
"@storybook/theming": "5.2.0-alpha.23",
"cors": "^2.8.5",
"cross-env": "^5.2.0",
diff --git a/examples/official-storybook/webpack.config.js b/examples/official-storybook/webpack.config.js
index 3429adc0ed4d..b027a4b06a0e 100644
--- a/examples/official-storybook/webpack.config.js
+++ b/examples/official-storybook/webpack.config.js
@@ -39,8 +39,13 @@ module.exports = async ({ config }) => ({
exclude: [/node_modules/, /dist/],
},
{
- test: /\.stories\.jsx?$/,
- use: require.resolve('@storybook/addon-storysource/loader'),
+ test: /\.stories\.[tj]sx?$/,
+ use: [
+ {
+ loader: require.resolve('@storybook/source-loader'),
+ options: { injectParameters: true },
+ },
+ ],
include: [
path.resolve(__dirname, './stories'),
path.resolve(__dirname, '../../lib/ui/src'),
diff --git a/examples/polymer-cli/.storybook/webpack.config.js b/examples/polymer-cli/.storybook/webpack.config.js
index 3e7ae19f52be..0be010861597 100644
--- a/examples/polymer-cli/.storybook/webpack.config.js
+++ b/examples/polymer-cli/.storybook/webpack.config.js
@@ -4,7 +4,7 @@ const webpack = require('webpack');
module.exports = async ({ config }) => {
config.module.rules.push({
test: [/\.stories\.js$/, /index\.js$/],
- loaders: [require.resolve('@storybook/addon-storysource/loader')],
+ loaders: [require.resolve('@storybook/source-loader')],
include: [path.resolve(__dirname, '../src')],
enforce: 'pre',
});
diff --git a/examples/polymer-cli/package.json b/examples/polymer-cli/package.json
index ba93e8862d40..f4753f186cb0 100644
--- a/examples/polymer-cli/package.json
+++ b/examples/polymer-cli/package.json
@@ -20,6 +20,7 @@
"@storybook/addon-storysource": "5.2.0-alpha.23",
"@storybook/addon-viewport": "5.2.0-alpha.23",
"@storybook/polymer": "5.2.0-alpha.23",
+ "@storybook/source-loader": "5.2.0-alpha.23",
"@webcomponents/webcomponentsjs": "^1.2.0",
"global": "^4.3.2",
"lit-html": "^1.0.0",
diff --git a/examples/preact-kitchen-sink/.storybook/webpack.config.js b/examples/preact-kitchen-sink/.storybook/webpack.config.js
index 46ba330e6665..c8a75926b7ca 100644
--- a/examples/preact-kitchen-sink/.storybook/webpack.config.js
+++ b/examples/preact-kitchen-sink/.storybook/webpack.config.js
@@ -3,7 +3,7 @@ const path = require('path');
module.exports = ({ config }) => {
config.module.rules.push({
test: [/\.stories\.js$/],
- loaders: [require.resolve('@storybook/addon-storysource/loader')],
+ loaders: [require.resolve('@storybook/source-loader')],
include: [path.resolve(__dirname, '../src')],
enforce: 'pre',
});
diff --git a/examples/preact-kitchen-sink/package.json b/examples/preact-kitchen-sink/package.json
index 5b3d6aa7a813..230d12286847 100644
--- a/examples/preact-kitchen-sink/package.json
+++ b/examples/preact-kitchen-sink/package.json
@@ -30,6 +30,7 @@
"@storybook/addon-viewport": "5.2.0-alpha.23",
"@storybook/addons": "5.2.0-alpha.23",
"@storybook/preact": "5.2.0-alpha.23",
+ "@storybook/source-loader": "5.2.0-alpha.23",
"babel-loader": "^8.0.4",
"cross-env": "^5.2.0",
"file-loader": "^3.0.1",
diff --git a/examples/rax-kitchen-sink/.storybook/webpack.config.js b/examples/rax-kitchen-sink/.storybook/webpack.config.js
index 99e298d739f2..39dff33950d1 100644
--- a/examples/rax-kitchen-sink/.storybook/webpack.config.js
+++ b/examples/rax-kitchen-sink/.storybook/webpack.config.js
@@ -5,7 +5,7 @@ module.exports = {
rules: [
{
test: [/\.stories\.js$/, /index\.js$/],
- loaders: [require.resolve('@storybook/addon-storysource/loader')],
+ loaders: [require.resolve('@storybook/source-loader')],
include: [path.resolve(__dirname, '../src')],
enforce: 'pre',
},
diff --git a/examples/rax-kitchen-sink/package.json b/examples/rax-kitchen-sink/package.json
index 98ca817db918..bb929a018746 100644
--- a/examples/rax-kitchen-sink/package.json
+++ b/examples/rax-kitchen-sink/package.json
@@ -40,6 +40,7 @@
"@storybook/addon-viewport": "5.2.0-alpha.23",
"@storybook/addons": "5.2.0-alpha.23",
"@storybook/rax": "5.2.0-alpha.23",
+ "@storybook/source-loader": "5.2.0-alpha.23",
"babel-eslint": "^8.2.2",
"babel-preset-rax": "^1.0.0-beta.0",
"rax-scripts": "^1.0.0-beta.10",
diff --git a/examples/riot-kitchen-sink/.storybook/webpack.config.js b/examples/riot-kitchen-sink/.storybook/webpack.config.js
index 9eb381789988..5f6354c3c594 100644
--- a/examples/riot-kitchen-sink/.storybook/webpack.config.js
+++ b/examples/riot-kitchen-sink/.storybook/webpack.config.js
@@ -3,7 +3,7 @@ const path = require('path');
module.exports = async ({ config }) => {
config.module.rules.push({
test: [/\.stories\.js$/, /index\.js$/],
- loaders: [require.resolve('@storybook/addon-storysource/loader')],
+ loaders: [require.resolve('@storybook/source-loader')],
include: [path.resolve(__dirname, '../src')],
enforce: 'pre',
});
diff --git a/examples/riot-kitchen-sink/package.json b/examples/riot-kitchen-sink/package.json
index cf3f892e7380..275d1c24e000 100644
--- a/examples/riot-kitchen-sink/package.json
+++ b/examples/riot-kitchen-sink/package.json
@@ -29,6 +29,7 @@
"@storybook/addon-viewport": "5.2.0-alpha.23",
"@storybook/addons": "5.2.0-alpha.23",
"@storybook/riot": "5.2.0-alpha.23",
+ "@storybook/source-loader": "5.2.0-alpha.23",
"babel-loader": "^8.0.4",
"cross-env": "^5.2.0",
"file-loader": "^3.0.1",
diff --git a/examples/svelte-kitchen-sink/.storybook/webpack.config.js b/examples/svelte-kitchen-sink/.storybook/webpack.config.js
index a943831441ac..243b6489507a 100644
--- a/examples/svelte-kitchen-sink/.storybook/webpack.config.js
+++ b/examples/svelte-kitchen-sink/.storybook/webpack.config.js
@@ -3,7 +3,7 @@ const path = require('path');
module.exports = async ({ config }) => {
config.module.rules.push({
test: [/\.stories\.js$/, /index\.js$/],
- loaders: [require.resolve('@storybook/addon-storysource/loader')],
+ loaders: [require.resolve('@storybook/source-loader')],
include: [path.resolve(__dirname, '../src')],
enforce: 'pre',
});
diff --git a/examples/svelte-kitchen-sink/package.json b/examples/svelte-kitchen-sink/package.json
index 4eac0f34f862..e01a46e3cf4f 100644
--- a/examples/svelte-kitchen-sink/package.json
+++ b/examples/svelte-kitchen-sink/package.json
@@ -23,6 +23,7 @@
"@storybook/addon-storysource": "5.2.0-alpha.23",
"@storybook/addon-viewport": "5.2.0-alpha.23",
"@storybook/addons": "5.2.0-alpha.23",
+ "@storybook/source-loader": "5.2.0-alpha.23",
"@storybook/svelte": "5.2.0-alpha.23"
}
}
diff --git a/examples/vue-kitchen-sink/.storybook/webpack.config.js b/examples/vue-kitchen-sink/.storybook/webpack.config.js
index a943831441ac..243b6489507a 100644
--- a/examples/vue-kitchen-sink/.storybook/webpack.config.js
+++ b/examples/vue-kitchen-sink/.storybook/webpack.config.js
@@ -3,7 +3,7 @@ const path = require('path');
module.exports = async ({ config }) => {
config.module.rules.push({
test: [/\.stories\.js$/, /index\.js$/],
- loaders: [require.resolve('@storybook/addon-storysource/loader')],
+ loaders: [require.resolve('@storybook/source-loader')],
include: [path.resolve(__dirname, '../src')],
enforce: 'pre',
});
diff --git a/examples/vue-kitchen-sink/package.json b/examples/vue-kitchen-sink/package.json
index 68e71bddc0a9..03ed7ca76e4c 100644
--- a/examples/vue-kitchen-sink/package.json
+++ b/examples/vue-kitchen-sink/package.json
@@ -29,6 +29,7 @@
"@storybook/addon-storysource": "5.2.0-alpha.23",
"@storybook/addon-viewport": "5.2.0-alpha.23",
"@storybook/addons": "5.2.0-alpha.23",
+ "@storybook/source-loader": "5.2.0-alpha.23",
"@storybook/vue": "5.2.0-alpha.23",
"babel-core": "^7.0.0-bridge.0",
"babel-loader": "^8.0.5",
diff --git a/lib/source-loader/package.json b/lib/source-loader/package.json
new file mode 100644
index 000000000000..60d4a783a1c4
--- /dev/null
+++ b/lib/source-loader/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@storybook/source-loader",
+ "version": "5.2.0-alpha.23",
+ "description": "Source loader",
+ "keywords": [
+ "lib",
+ "storybook"
+ ],
+ "homepage": "https://github.com/storybookjs/storybook/tree/master/lib/source-loader",
+ "bugs": {
+ "url": "https://github.com/storybookjs/storybook/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/storybookjs/storybook.git",
+ "directory": "lib/source-loader"
+ },
+ "license": "MIT",
+ "main": "dist/index.js",
+ "jsnext:main": "src/index.js",
+ "scripts": {
+ "prepare": "node ../../scripts/prepare.js"
+ },
+ "dependencies": {
+ "@storybook/addons": "5.2.0-alpha.23",
+ "@storybook/router": "5.2.0-alpha.23",
+ "core-js": "^3.0.1",
+ "estraverse": "^4.2.0",
+ "global": "^4.3.2",
+ "loader-utils": "^1.2.3",
+ "prettier": "^1.16.4",
+ "prop-types": "^15.7.2",
+ "regenerator-runtime": "^0.12.1"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/lib/source-loader/src/abstract-syntax-tree/__snapshots__/inject-decorator.test.js.snap b/lib/source-loader/src/abstract-syntax-tree/__snapshots__/inject-decorator.test.js.snap
new file mode 100644
index 000000000000..6cd7c5d832e7
--- /dev/null
+++ b/lib/source-loader/src/abstract-syntax-tree/__snapshots__/inject-decorator.test.js.snap
@@ -0,0 +1,858 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`inject-decorator injectDecorator option is false - angular does not inject stories decorator after the all "storiesOf" functions 1`] = `
+"import { Component } from '@angular/core';
+import { storiesOf } from '@storybook/angular';
+
+@Component({
+ selector: 'storybook-with-ng-content',
+ template: \`
\`,
+})
+class WithNgContentComponent {}
+
+storiesOf('Custom|ng-content', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add('Default', () => ({
+ template: \`This is rendered in ng-content \`,
+ moduleMetadata: {
+ declarations: [WithNgContentComponent],
+ },
+}));
+"
+`;
+
+exports[`inject-decorator injectDecorator option is false - flow does not inject stories decorator after the all "storiesOf" functions 1`] = `
+"// @flow
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { withInfo } from '@storybook/addon-info';
+
+import TableComponent from '../components/TableComponent';
+
+import type { JssClasses } from '../types';
+
+type State = {
+ value: any,
+};
+
+type Props = {
+ classes: JssClasses,
+ name: string,
+};
+
+class Table extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ value: undefined,
+ };
+ }
+
+ state: State;
+
+ render() {
+ return ;
+ }
+}
+
+const stories = storiesOf('Table', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__));
+stories.add('Flow Class', withInfo('Lorum Ipsum Nem')(() => ));
+"
+`;
+
+exports[`inject-decorator injectDecorator option is false - ts does not inject stories decorator after the all "storiesOf" functions 1`] = `
+"var addSourceDecorator = require(\\"@storybook/source-loader\\").addSource;
+import { Component } from '@angular/core';
+import { Store, StoreModule } from '@ngrx/store';
+import { storiesOf, moduleMetadata } from '@storybook/angular';
+
+@Component({
+ selector: 'storybook-comp-with-store',
+ template: '{{this.getSotreState()}}
',
+})
+class WithStoreComponent {
+ private store: Store;
+
+ constructor(store: Store) {
+ this.store = store;
+ }
+
+ getSotreState(): string {
+ return this.store === undefined ? 'Store is NOT injected' : 'Store is injected';
+ }
+}
+
+storiesOf('ngrx|Store', module)
+ .addDecorator(
+ moduleMetadata({
+ imports: [StoreModule.forRoot({})],
+ declarations: [WithStoreComponent],
+ })
+ )
+ .add('With component', () => {
+ return {
+ component: WithStoreComponent,
+ };
+ });"
+`;
+
+exports[`inject-decorator injectDecorator option is false does not inject stories decorator after the all "storiesOf" functions 1`] = `
+"import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { withInfo } from '@storybook/addon-info';
+import { action } from '@storybook/addon-actions';
+
+import DocgenButton from '../components/DocgenButton';
+import FlowTypeButton from '../components/FlowTypeButton';
+import BaseButton from '../components/BaseButton';
+import TableComponent from '../components/TableComponent';
+
+storiesOf('Addons|Info.React Docgen', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__))
+ .add(
+ 'Comments from PropType declarations',
+ withInfo(
+ 'Comments above the PropType declarations should be extracted from the React component file itself and rendered in the Info Addon prop table'
+ )(() => )
+ )
+ .add(
+ 'Comments from Flow declarations',
+ withInfo(
+ 'Comments above the Flow declarations should be extracted from the React component file itself and rendered in the Info Addon prop table'
+ )(() => )
+ )
+ .add(
+ 'Comments from component declaration',
+ withInfo(
+ 'Comments above the component declaration should be extracted from the React component file itself and rendered below the Info Addon heading'
+ )(() => )
+ );
+
+const markdownDescription = \`
+#### You can use markdown in your withInfo() description.
+
+Sometimes you might want to manually include some code examples:
+~~~js
+const Button = () => ;
+~~~
+
+Maybe include a [link](http://storybook.js.org) to your project as well.
+\`;
+
+storiesOf('Addons|Info.Markdown', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Displays Markdown in description',
+ withInfo(markdownDescription)(() => )
+);
+
+storiesOf('Addons|Info.Options.inline', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Inlines component inside story',
+ withInfo({
+ text: 'Component should be inlined between description and PropType table',
+ inline: true, // Displays info inline vs click button to view
+ })(() => )
+);
+
+storiesOf('Addons|Info.Options.header', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Shows or hides Info Addon header',
+ withInfo({
+ text: 'The Info Addon header should be hidden',
+ header: false, // Toggles display of header with component name and description
+ })(() => )
+);
+
+storiesOf('Addons|Info.Options.source', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Shows or hides Info Addon source',
+ withInfo({
+ text: 'The Info Addon source section should be hidden',
+ source: false, // Displays the source of story Component
+ })(() => )
+);
+
+storiesOf('Addons|Info.Options.propTables', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Shows additional component prop tables',
+ withInfo({
+ text: 'There should be a prop table added for a component not included in the story',
+ propTables: [FlowTypeButton],
+ })(() => )
+);
+
+storiesOf('Addons|Info.Options.propTablesExclude', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Exclude component from prop tables',
+ withInfo({
+ text: 'This can exclude extraneous components from being displayed in prop tables.',
+ propTablesExclude: [FlowTypeButton],
+ })(() => (
+
+
+
+
+ ))
+);
+
+storiesOf('Addons|Info.Options.styles', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__))
+ .add(
+ 'Extend info styles with an object',
+ withInfo({
+ styles: {
+ button: {
+ base: {
+ background: 'purple',
+ },
+ },
+ header: {
+ h1: {
+ color: 'green',
+ },
+ },
+ },
+ })(() => )
+ )
+ .add(
+ 'Full control over styles using a function',
+ withInfo({
+ styles: stylesheet => ({
+ ...stylesheet,
+ header: {
+ ...stylesheet.header,
+ h1: {
+ ...stylesheet.header.h1,
+ color: 'red',
+ },
+ },
+ }),
+ })(() => )
+ );
+
+storiesOf('Addons|Info.Options.TableComponent', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Use a custom component for the table',
+ withInfo({
+ TableComponent,
+ })(() => )
+);
+
+storiesOf('Addons|Info.Decorator', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__))
+ .addDecorator((story, context) =>
+ withInfo('Info could be used as a global or local decorator as well.')(story)(context)
+ )
+ .add('Use Info as story decorator', () => );
+
+const hoc = WrapComponent => ({ ...props }) => ;
+
+const Input = hoc(() => );
+
+const TextArea = hoc(({ children }) => );
+
+storiesOf('Addons|Info.GitHub issues', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ '#1814',
+ withInfo('Allow Duplicate DisplayNames for HOC #1814')(() => (
+
+
+
+
+ ))
+);
+"
+`;
+
+exports[`inject-decorator positive - angular calculates "adds" map 1`] = `
+Object {
+ "custom-ng-content--default": Object {
+ "endBody": Object {
+ "col": 2,
+ "line": 15,
+ },
+ "endLoc": Object {
+ "col": 2,
+ "line": 15,
+ },
+ "startBody": Object {
+ "col": 54,
+ "line": 10,
+ },
+ "startLoc": Object {
+ "col": 43,
+ "line": 10,
+ },
+ },
+}
+`;
+
+exports[`inject-decorator positive - angular injects stories decorator after the all "storiesOf" functions 1`] = `
+"import { Component } from '@angular/core';
+import { storiesOf } from '@storybook/angular';
+
+@Component({
+ selector: 'storybook-with-ng-content',
+ template: \`
\`,
+})
+class WithNgContentComponent {}
+
+storiesOf('Custom|ng-content', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add('Default', () => ({
+ template: \`This is rendered in ng-content \`,
+ moduleMetadata: {
+ declarations: [WithNgContentComponent],
+ },
+}));
+"
+`;
+
+exports[`inject-decorator positive - flow calculates "adds" map 1`] = `Object {}`;
+
+exports[`inject-decorator positive - flow injects stories decorator after the all "storiesOf" functions 1`] = `
+"// @flow
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { withInfo } from '@storybook/addon-info';
+
+import TableComponent from '../components/TableComponent';
+
+import type { JssClasses } from '../types';
+
+type State = {
+ value: any,
+};
+
+type Props = {
+ classes: JssClasses,
+ name: string,
+};
+
+class Table extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ value: undefined,
+ };
+ }
+
+ state: State;
+
+ render() {
+ return ;
+ }
+}
+
+const stories = storiesOf('Table', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__));
+stories.add('Flow Class', withInfo('Lorum Ipsum Nem')(() => ));
+"
+`;
+
+exports[`inject-decorator positive - ts calculates "adds" map 1`] = `Object {}`;
+
+exports[`inject-decorator positive - ts injects stories decorator after the all "storiesOf" functions 1`] = `
+"var addSourceDecorator = require(\\"@storybook/source-loader\\").addSource;
+import { Component } from '@angular/core';
+import { Store, StoreModule } from '@ngrx/store';
+import { storiesOf, moduleMetadata } from '@storybook/angular';
+
+@Component({
+ selector: 'storybook-comp-with-store',
+ template: '{{this.getSotreState()}}
',
+})
+class WithStoreComponent {
+ private store: Store;
+
+ constructor(store: Store) {
+ this.store = store;
+ }
+
+ getSotreState(): string {
+ return this.store === undefined ? 'Store is NOT injected' : 'Store is injected';
+ }
+}
+
+storiesOf('ngrx|Store', module)
+ .addDecorator(
+ moduleMetadata({
+ imports: [StoreModule.forRoot({})],
+ declarations: [WithStoreComponent],
+ })
+ )
+ .add('With component', () => {
+ return {
+ component: WithStoreComponent,
+ };
+ });"
+`;
+
+exports[`inject-decorator positive calculates "adds" map 1`] = `
+Object {
+ "addons-info-decorator--use-info-as-story-decorator": Object {
+ "endBody": Object {
+ "col": 73,
+ "line": 137,
+ },
+ "endLoc": Object {
+ "col": 73,
+ "line": 137,
+ },
+ "startBody": Object {
+ "col": 38,
+ "line": 137,
+ },
+ "startLoc": Object {
+ "col": 7,
+ "line": 137,
+ },
+ },
+ "addons-info-github-issues--1814": Object {
+ "endBody": Object {
+ "col": 4,
+ "line": 152,
+ },
+ "endLoc": Object {
+ "col": 4,
+ "line": 152,
+ },
+ "startBody": Object {
+ "col": 2,
+ "line": 147,
+ },
+ "startLoc": Object {
+ "col": 2,
+ "line": 146,
+ },
+ },
+ "addons-info-markdown--displays-markdown-in-description": Object {
+ "endBody": Object {
+ "col": 96,
+ "line": 44,
+ },
+ "endLoc": Object {
+ "col": 96,
+ "line": 44,
+ },
+ "startBody": Object {
+ "col": 2,
+ "line": 44,
+ },
+ "startLoc": Object {
+ "col": 2,
+ "line": 43,
+ },
+ },
+ "addons-info-options-header--shows-or-hides-info-addon-header": Object {
+ "endBody": Object {
+ "col": 41,
+ "line": 60,
+ },
+ "endLoc": Object {
+ "col": 41,
+ "line": 60,
+ },
+ "startBody": Object {
+ "col": 2,
+ "line": 57,
+ },
+ "startLoc": Object {
+ "col": 2,
+ "line": 56,
+ },
+ },
+ "addons-info-options-inline--inlines-component-inside-story": Object {
+ "endBody": Object {
+ "col": 41,
+ "line": 52,
+ },
+ "endLoc": Object {
+ "col": 41,
+ "line": 52,
+ },
+ "startBody": Object {
+ "col": 2,
+ "line": 49,
+ },
+ "startLoc": Object {
+ "col": 2,
+ "line": 48,
+ },
+ },
+ "addons-info-options-proptables--shows-additional-component-prop-tables": Object {
+ "endBody": Object {
+ "col": 41,
+ "line": 76,
+ },
+ "endLoc": Object {
+ "col": 41,
+ "line": 76,
+ },
+ "startBody": Object {
+ "col": 2,
+ "line": 73,
+ },
+ "startLoc": Object {
+ "col": 2,
+ "line": 72,
+ },
+ },
+ "addons-info-options-proptablesexclude--exclude-component-from-prop-tables": Object {
+ "endBody": Object {
+ "col": 4,
+ "line": 89,
+ },
+ "endLoc": Object {
+ "col": 4,
+ "line": 89,
+ },
+ "startBody": Object {
+ "col": 2,
+ "line": 81,
+ },
+ "startLoc": Object {
+ "col": 2,
+ "line": 80,
+ },
+ },
+ "addons-info-options-source--shows-or-hides-info-addon-source": Object {
+ "endBody": Object {
+ "col": 41,
+ "line": 68,
+ },
+ "endLoc": Object {
+ "col": 41,
+ "line": 68,
+ },
+ "startBody": Object {
+ "col": 2,
+ "line": 65,
+ },
+ "startLoc": Object {
+ "col": 2,
+ "line": 64,
+ },
+ },
+ "addons-info-options-styles--extend-info-styles-with-an-object": Object {
+ "endBody": Object {
+ "col": 43,
+ "line": 108,
+ },
+ "endLoc": Object {
+ "col": 43,
+ "line": 108,
+ },
+ "startBody": Object {
+ "col": 4,
+ "line": 95,
+ },
+ "startLoc": Object {
+ "col": 4,
+ "line": 94,
+ },
+ },
+ "addons-info-options-styles--full-control-over-styles-using-a-function": Object {
+ "endBody": Object {
+ "col": 43,
+ "line": 123,
+ },
+ "endLoc": Object {
+ "col": 43,
+ "line": 123,
+ },
+ "startBody": Object {
+ "col": 4,
+ "line": 112,
+ },
+ "startLoc": Object {
+ "col": 4,
+ "line": 111,
+ },
+ },
+ "addons-info-options-tablecomponent--use-a-custom-component-for-the-table": Object {
+ "endBody": Object {
+ "col": 41,
+ "line": 130,
+ },
+ "endLoc": Object {
+ "col": 41,
+ "line": 130,
+ },
+ "startBody": Object {
+ "col": 2,
+ "line": 128,
+ },
+ "startLoc": Object {
+ "col": 2,
+ "line": 127,
+ },
+ },
+ "addons-info-react-docgen--comments-from-component-declaration": Object {
+ "endBody": Object {
+ "col": 70,
+ "line": 28,
+ },
+ "endLoc": Object {
+ "col": 70,
+ "line": 28,
+ },
+ "startBody": Object {
+ "col": 4,
+ "line": 26,
+ },
+ "startLoc": Object {
+ "col": 4,
+ "line": 25,
+ },
+ },
+ "addons-info-react-docgen--comments-from-flow-declarations": Object {
+ "endBody": Object {
+ "col": 85,
+ "line": 22,
+ },
+ "endLoc": Object {
+ "col": 85,
+ "line": 22,
+ },
+ "startBody": Object {
+ "col": 4,
+ "line": 20,
+ },
+ "startLoc": Object {
+ "col": 4,
+ "line": 19,
+ },
+ },
+ "addons-info-react-docgen--comments-from-proptype-declarations": Object {
+ "endBody": Object {
+ "col": 79,
+ "line": 16,
+ },
+ "endLoc": Object {
+ "col": 79,
+ "line": 16,
+ },
+ "startBody": Object {
+ "col": 4,
+ "line": 14,
+ },
+ "startLoc": Object {
+ "col": 4,
+ "line": 13,
+ },
+ },
+}
+`;
+
+exports[`inject-decorator positive injects stories decorator after the all "storiesOf" functions 1`] = `
+"import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { withInfo } from '@storybook/addon-info';
+import { action } from '@storybook/addon-actions';
+
+import DocgenButton from '../components/DocgenButton';
+import FlowTypeButton from '../components/FlowTypeButton';
+import BaseButton from '../components/BaseButton';
+import TableComponent from '../components/TableComponent';
+
+storiesOf('Addons|Info.React Docgen', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__))
+ .add(
+ 'Comments from PropType declarations',
+ withInfo(
+ 'Comments above the PropType declarations should be extracted from the React component file itself and rendered in the Info Addon prop table'
+ )(() => )
+ )
+ .add(
+ 'Comments from Flow declarations',
+ withInfo(
+ 'Comments above the Flow declarations should be extracted from the React component file itself and rendered in the Info Addon prop table'
+ )(() => )
+ )
+ .add(
+ 'Comments from component declaration',
+ withInfo(
+ 'Comments above the component declaration should be extracted from the React component file itself and rendered below the Info Addon heading'
+ )(() => )
+ );
+
+const markdownDescription = \`
+#### You can use markdown in your withInfo() description.
+
+Sometimes you might want to manually include some code examples:
+~~~js
+const Button = () => ;
+~~~
+
+Maybe include a [link](http://storybook.js.org) to your project as well.
+\`;
+
+storiesOf('Addons|Info.Markdown', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Displays Markdown in description',
+ withInfo(markdownDescription)(() => )
+);
+
+storiesOf('Addons|Info.Options.inline', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Inlines component inside story',
+ withInfo({
+ text: 'Component should be inlined between description and PropType table',
+ inline: true, // Displays info inline vs click button to view
+ })(() => )
+);
+
+storiesOf('Addons|Info.Options.header', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Shows or hides Info Addon header',
+ withInfo({
+ text: 'The Info Addon header should be hidden',
+ header: false, // Toggles display of header with component name and description
+ })(() => )
+);
+
+storiesOf('Addons|Info.Options.source', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Shows or hides Info Addon source',
+ withInfo({
+ text: 'The Info Addon source section should be hidden',
+ source: false, // Displays the source of story Component
+ })(() => )
+);
+
+storiesOf('Addons|Info.Options.propTables', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Shows additional component prop tables',
+ withInfo({
+ text: 'There should be a prop table added for a component not included in the story',
+ propTables: [FlowTypeButton],
+ })(() => )
+);
+
+storiesOf('Addons|Info.Options.propTablesExclude', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Exclude component from prop tables',
+ withInfo({
+ text: 'This can exclude extraneous components from being displayed in prop tables.',
+ propTablesExclude: [FlowTypeButton],
+ })(() => (
+
+
+
+
+ ))
+);
+
+storiesOf('Addons|Info.Options.styles', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__))
+ .add(
+ 'Extend info styles with an object',
+ withInfo({
+ styles: {
+ button: {
+ base: {
+ background: 'purple',
+ },
+ },
+ header: {
+ h1: {
+ color: 'green',
+ },
+ },
+ },
+ })(() => )
+ )
+ .add(
+ 'Full control over styles using a function',
+ withInfo({
+ styles: stylesheet => ({
+ ...stylesheet,
+ header: {
+ ...stylesheet.header,
+ h1: {
+ ...stylesheet.header.h1,
+ color: 'red',
+ },
+ },
+ }),
+ })(() => )
+ );
+
+storiesOf('Addons|Info.Options.TableComponent', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ 'Use a custom component for the table',
+ withInfo({
+ TableComponent,
+ })(() => )
+);
+
+storiesOf('Addons|Info.Decorator', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__))
+ .addDecorator((story, context) =>
+ withInfo('Info could be used as a global or local decorator as well.')(story)(context)
+ )
+ .add('Use Info as story decorator', () => );
+
+const hoc = WrapComponent => ({ ...props }) => ;
+
+const Input = hoc(() => );
+
+const TextArea = hoc(({ children }) => );
+
+storiesOf('Addons|Info.GitHub issues', module).addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__)).add(
+ '#1814',
+ withInfo('Allow Duplicate DisplayNames for HOC #1814')(() => (
+
+
+
+
+ ))
+);
+"
+`;
+
+exports[`inject-decorator stories with ugly comments in ts should delete ugly comments from the generated story source 1`] = `
+"/* global window */
+/* eslint-disable global-require, import/no-dynamic-require */
+
+import React from 'react';
+
+@Component({
+ selector: 'storybook-comp-with-store',
+ template: '{{this.getSotreState()}}
',
+})
+class WithStoreComponent {
+ private store: Store;
+
+ constructor(store: Store) {
+ this.store = store;
+ }
+
+ getSotreState(): string {
+ return this.store === undefined ? 'Store is NOT injected' : 'Store is injected';
+ }
+}
+
+/*
+ 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.
+*/"
+`;
+
+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`] = `
+"var addSourceDecorator = require(\\"@storybook/source-loader\\").addSource;
+while(true) {
+ console.log(\\"it's a kind of magic\\");
+}"
+`;
diff --git a/lib/source-loader/src/abstract-syntax-tree/default-options.js b/lib/source-loader/src/abstract-syntax-tree/default-options.js
new file mode 100644
index 000000000000..65ca9c768a28
--- /dev/null
+++ b/lib/source-loader/src/abstract-syntax-tree/default-options.js
@@ -0,0 +1,12 @@
+const defaultOptions = {
+ prettierConfig: {
+ printWidth: 100,
+ tabWidth: 2,
+ bracketSpacing: true,
+ trailingComma: 'es5',
+ singleQuote: true,
+ },
+ uglyCommentsRegex: [/^eslint-.*/, /^global.*/],
+};
+
+export default defaultOptions;
diff --git a/lib/source-loader/src/abstract-syntax-tree/generate-helpers.js b/lib/source-loader/src/abstract-syntax-tree/generate-helpers.js
new file mode 100644
index 000000000000..a79e96fec7d6
--- /dev/null
+++ b/lib/source-loader/src/abstract-syntax-tree/generate-helpers.js
@@ -0,0 +1,172 @@
+import { patchNode } from './parse-helpers';
+import getParser from './parsers';
+import {
+ splitSTORYOF,
+ findAddsMap,
+ findDependencies,
+ splitExports,
+ popParametersObjectFromDefaultExport,
+ findExportsMap as generateExportsMap,
+} from './traverse-helpers';
+
+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))
+ .map(patchNode)
+ .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, parser, filepath }) {
+ let config = prettierConfig;
+ let foundParser = null;
+ if (parser === 'flow') foundParser = 'flow';
+ if (parser === 'javascript' || /jsx?/.test(parser)) foundParser = 'javascript';
+ if (parser === 'typescript' || /tsx?/.test(parser)) foundParser = 'typescript';
+
+ if (!config.parser) {
+ config = {
+ ...prettierConfig,
+ };
+ } else if (filepath) {
+ config = {
+ ...prettierConfig,
+ filepath,
+ };
+ } else {
+ config = {
+ ...prettierConfig,
+ };
+ }
+
+ try {
+ return getParser(foundParser || 'javascript').format(source, config);
+ } catch (e) {
+ // Can fail when the source is a JSON
+ return source;
+ }
+}
+
+const STORY_DECORATOR_STATEMENT =
+ '.addDecorator(withSourceLoader(__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__))';
+const ADD_PARAMETERS_STATEMENT =
+ '.addParameters({ storySource: { source: __STORY__, locationsMap: __ADDS_MAP__ } })';
+const IMPORT_DECLARATION_FOR_EXPORTED_STORIES_DECORATOR =
+ 'var addSourceDecorator = require("@storybook/source-loader").addSource;\n';
+const applyExportDecoratorStatement = part =>
+ ` addSourceDecorator(${part}, {__STORY__, __ADDS_MAP__,__MAIN_FILE_LOCATION__,__MODULE_DEPENDENCIES__,__LOCAL_DEPENDENCIES__,__SOURCE_PREFIX__,__IDS_TO_FRAMEWORKS__});`;
+
+export function generateSourceWithDecorators(source, ast, withParameters) {
+ const { comments = [] } = ast;
+
+ const partsUsingStoryOfToken = splitSTORYOF(ast, source);
+
+ if (partsUsingStoryOfToken.length > 1) {
+ const newSource = partsUsingStoryOfToken.join(
+ (withParameters ? ADD_PARAMETERS_STATEMENT : '') + STORY_DECORATOR_STATEMENT
+ );
+
+ return {
+ storyOfTokenFound: true,
+ changed: partsUsingStoryOfToken.length > 1,
+ source: newSource,
+ comments,
+ };
+ }
+
+ const partsUsingExports = splitExports(ast, source);
+
+ const newSource =
+ IMPORT_DECLARATION_FOR_EXPORTED_STORIES_DECORATOR +
+ partsUsingExports
+ .map((part, i) => (i % 2 === 0 ? part : applyExportDecoratorStatement(part)))
+ .join('');
+
+ return {
+ exportTokenFound: true,
+ changed: partsUsingExports.length > 1,
+ source: newSource,
+ comments,
+ };
+}
+
+export function generateSourceWithoutDecorators(source, ast) {
+ const { comments = [] } = ast;
+
+ return {
+ changed: true,
+ source,
+ comments,
+ };
+}
+
+export function generateAddsMap(ast, storiesOfIdentifiers) {
+ return findAddsMap(ast, storiesOfIdentifiers);
+}
+
+export function generateStoriesLocationsMap(ast, storiesOfIdentifiers) {
+ const usingAddsMap = generateAddsMap(ast, storiesOfIdentifiers);
+ const { addsMap } = usingAddsMap;
+
+ if (Object.keys(addsMap).length > 0) {
+ return usingAddsMap;
+ }
+ const usingExportsMap = generateExportsMap(ast);
+
+ return usingExportsMap || usingAddsMap;
+}
+
+export function generateDependencies(ast) {
+ return findDependencies(ast);
+}
+
+export function generateStorySource({ source, ...options }) {
+ let storySource = source;
+
+ storySource = generateSourceWithoutUglyComments(storySource, options);
+ storySource = prettifyCode(storySource, options);
+
+ return storySource;
+}
+
+export function generateSourcesInExportedParameters(source, ast, additionalParameters) {
+ const {
+ splicedSource,
+ parametersSliceOfCode,
+ indexWhereToAppend,
+ foundParametersProperty,
+ } = popParametersObjectFromDefaultExport(source, ast);
+ if (indexWhereToAppend !== -1) {
+ const additionalParametersAsJson = JSON.stringify({ storySource: additionalParameters }).slice(
+ 0,
+ -1
+ );
+ const propertyDeclaration = foundParametersProperty ? '' : 'parameters: ';
+ const comma = foundParametersProperty ? '' : ',';
+ const newParameters = `${propertyDeclaration}${additionalParametersAsJson},${parametersSliceOfCode.substring(
+ 1
+ )}${comma}`;
+ const result =
+ splicedSource.substring(0, indexWhereToAppend) +
+ newParameters +
+ splicedSource.substring(indexWhereToAppend);
+ return result;
+ }
+ return source;
+}
diff --git a/lib/source-loader/src/abstract-syntax-tree/inject-decorator.js b/lib/source-loader/src/abstract-syntax-tree/inject-decorator.js
new file mode 100644
index 000000000000..aa620eee0392
--- /dev/null
+++ b/lib/source-loader/src/abstract-syntax-tree/inject-decorator.js
@@ -0,0 +1,84 @@
+import defaultOptions from './default-options';
+import getParser from './parsers';
+
+import {
+ generateSourceWithDecorators,
+ generateSourceWithoutDecorators,
+ generateStorySource,
+ generateStoriesLocationsMap,
+ generateDependencies,
+ generateSourcesInExportedParameters,
+} from './generate-helpers';
+
+function extendOptions(source, comments, filepath, options) {
+ return {
+ ...defaultOptions,
+ ...options,
+ source,
+ comments,
+ filepath,
+ };
+}
+
+function inject(source, filepath, options = {}, log = message => {}) {
+ const { injectDecorator = true } = options;
+ const obviouslyNotCode = ['md', 'txt', 'json'].includes(options.parser);
+ let parser = null;
+ try {
+ parser = getParser(options.parser);
+ } catch (e) {
+ log(new Error(`(not fatal, only impacting storysource) Could not load a parser (${e})`));
+ }
+
+ if (obviouslyNotCode || !parser) {
+ return {
+ source,
+ storySource: {},
+ addsMap: {},
+ changed: false,
+ dependencies: [],
+ };
+ }
+ const ast = parser.parse(source);
+
+ const { changed, source: cleanedSource, comments, exportTokenFound } =
+ injectDecorator === true
+ ? generateSourceWithDecorators(source, ast, options.injectParameters)
+ : generateSourceWithoutDecorators(source, ast);
+
+ const storySource = generateStorySource(extendOptions(source, comments, filepath, options));
+ const newAst = parser.parse(storySource);
+ const { dependencies, storiesOfIdentifiers } = generateDependencies(newAst);
+ const { addsMap, idsToFrameworks } = generateStoriesLocationsMap(newAst, storiesOfIdentifiers);
+
+ let newSource = cleanedSource;
+ if (exportTokenFound) {
+ const cleanedSourceAst = parser.parse(cleanedSource);
+ newSource = generateSourcesInExportedParameters(cleanedSource, cleanedSourceAst, {
+ source: storySource,
+ locationsMap: addsMap,
+ });
+ }
+
+ if (!changed && Object.keys(addsMap || {}).length === 0) {
+ return {
+ source: newSource,
+ storySource,
+ addsMap: {},
+ changed,
+ dependencies,
+ idsToFrameworks: idsToFrameworks || {},
+ };
+ }
+
+ return {
+ source: newSource,
+ storySource,
+ addsMap,
+ changed,
+ dependencies,
+ idsToFrameworks: idsToFrameworks || {},
+ };
+}
+
+export default inject;
diff --git a/lib/source-loader/src/abstract-syntax-tree/inject-decorator.test.js b/lib/source-loader/src/abstract-syntax-tree/inject-decorator.test.js
new file mode 100644
index 000000000000..17023390707e
--- /dev/null
+++ b/lib/source-loader/src/abstract-syntax-tree/inject-decorator.test.js
@@ -0,0 +1,216 @@
+import fs from 'fs';
+import path from 'path';
+import injectDecorator from './inject-decorator';
+
+const ADD_DECORATOR_STATEMENT = '.addDecorator(withStorySource(__STORY__, __ADDS_MAP__))';
+
+describe('inject-decorator', () => {
+ describe('positive', () => {
+ const mockFilePath = './__mocks__/inject-decorator.stories.txt';
+ const source = fs.readFileSync(mockFilePath, 'utf-8');
+ const result = injectDecorator(
+ source,
+ ADD_DECORATOR_STATEMENT,
+ path.resolve(__dirname, mockFilePath),
+ { parser: 'javascript' }
+ );
+
+ it('returns "changed" flag', () => {
+ expect(result.changed).toBeTruthy();
+ });
+
+ it('injects stories decorator after the all "storiesOf" functions', () => {
+ expect(result.source).toMatchSnapshot();
+ });
+
+ it('calculates "adds" map', () => {
+ expect(result.addsMap).toMatchSnapshot();
+ });
+ });
+
+ describe('positive - angular', () => {
+ const mockFilePath = './__mocks__/inject-decorator.angular-stories.txt';
+ const source = fs.readFileSync(mockFilePath, 'utf-8');
+ const result = injectDecorator(
+ source,
+ ADD_DECORATOR_STATEMENT,
+ path.resolve(__dirname, mockFilePath),
+ { parser: 'typescript' }
+ );
+
+ it('returns "changed" flag', () => {
+ expect(result.changed).toBeTruthy();
+ });
+
+ it('injects stories decorator after the all "storiesOf" functions', () => {
+ expect(result.source).toMatchSnapshot();
+ });
+
+ it('calculates "adds" map', () => {
+ expect(result.addsMap).toMatchSnapshot();
+ });
+ });
+
+ describe('positive - flow', () => {
+ const mockFilePath = './__mocks__/inject-decorator.flow-stories.txt';
+ const source = fs.readFileSync(mockFilePath, 'utf-8');
+ const result = injectDecorator(
+ source,
+ ADD_DECORATOR_STATEMENT,
+ path.resolve(__dirname, mockFilePath),
+ { parser: 'flow' }
+ );
+
+ it('returns "changed" flag', () => {
+ expect(result.changed).toBeTruthy();
+ });
+
+ it('injects stories decorator after the all "storiesOf" functions', () => {
+ expect(result.source).toMatchSnapshot();
+ });
+
+ it('calculates "adds" map', () => {
+ expect(result.addsMap).toMatchSnapshot();
+ });
+ });
+
+ describe('positive - ts', () => {
+ const mockFilePath = './__mocks__/inject-decorator.ts.txt';
+ const source = fs.readFileSync(mockFilePath, 'utf-8');
+ const result = injectDecorator(
+ source,
+ ADD_DECORATOR_STATEMENT,
+ path.resolve(__dirname, mockFilePath),
+ { parser: 'typescript' }
+ );
+
+ it('returns "changed" flag', () => {
+ expect(result.changed).toBeTruthy();
+ });
+
+ it('injects stories decorator after the all "storiesOf" functions', () => {
+ expect(result.source).toMatchSnapshot();
+ });
+
+ it('calculates "adds" map', () => {
+ expect(result.addsMap).toMatchSnapshot();
+ });
+ });
+
+ describe('stories with ugly comments', () => {
+ const mockFilePath = './__mocks__/inject-decorator.ugly-comments-stories.txt';
+ const source = fs.readFileSync(mockFilePath, 'utf-8');
+ const result = injectDecorator(
+ source,
+ ADD_DECORATOR_STATEMENT,
+ path.resolve(__dirname, mockFilePath),
+ { parser: 'javascript' }
+ );
+
+ it('should delete ugly comments from the generated story source', () => {
+ expect(result.storySource).toMatchSnapshot();
+ });
+ });
+
+ describe('stories with ugly comments in ts', () => {
+ const mockFilePath = './__mocks__/inject-decorator.ts.ugly-comments-stories.txt';
+ const source = fs.readFileSync(mockFilePath, 'utf-8');
+ const result = injectDecorator(
+ source,
+ ADD_DECORATOR_STATEMENT,
+ path.resolve(__dirname, mockFilePath),
+ { parser: 'typescript' }
+ );
+
+ 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 mockFilePath = './__mocks__/inject-decorator.no-stories.txt';
+ const source = fs.readFileSync(mockFilePath, 'utf-8');
+
+ const result = injectDecorator(
+ source,
+ ADD_DECORATOR_STATEMENT,
+ path.resolve(__dirname, mockFilePath)
+ );
+
+ expect(result.changed).toBeFalsy();
+ expect(result.addsMap).toEqual({});
+ expect(result.source).toMatchSnapshot();
+ });
+
+ describe('injectDecorator option is false', () => {
+ const mockFilePath = './__mocks__/inject-decorator.stories.txt';
+ const source = fs.readFileSync(mockFilePath, 'utf-8');
+ const result = injectDecorator(
+ source,
+ ADD_DECORATOR_STATEMENT,
+ path.resolve(__dirname, mockFilePath),
+ {
+ injectDecorator: false,
+ parser: 'javascript',
+ }
+ );
+
+ it('does not inject stories decorator after the all "storiesOf" functions', () => {
+ expect(result.source).toMatchSnapshot();
+ });
+ });
+
+ describe('injectDecorator option is false - angular', () => {
+ const mockFilePath = './__mocks__/inject-decorator.angular-stories.txt';
+ const source = fs.readFileSync(mockFilePath, 'utf-8');
+ const result = injectDecorator(
+ source,
+ ADD_DECORATOR_STATEMENT,
+ path.resolve(__dirname, mockFilePath),
+ {
+ injectDecorator: false,
+ parser: 'typescript',
+ }
+ );
+
+ it('does not inject stories decorator after the all "storiesOf" functions', () => {
+ expect(result.source).toMatchSnapshot();
+ });
+ });
+
+ describe('injectDecorator option is false - flow', () => {
+ const mockFilePath = './__mocks__/inject-decorator.flow-stories.txt';
+ const source = fs.readFileSync(mockFilePath, 'utf-8');
+ const result = injectDecorator(
+ source,
+ ADD_DECORATOR_STATEMENT,
+ path.resolve(__dirname, mockFilePath),
+ {
+ injectDecorator: false,
+ parser: 'flow',
+ }
+ );
+
+ it('does not inject stories decorator after the all "storiesOf" functions', () => {
+ expect(result.source).toMatchSnapshot();
+ });
+ });
+
+ describe('injectDecorator option is false - ts', () => {
+ const mockFilePath = './__mocks__/inject-decorator.ts.txt';
+ const source = fs.readFileSync(mockFilePath, 'utf-8');
+ const result = injectDecorator(
+ source,
+ ADD_DECORATOR_STATEMENT,
+ path.resolve(__dirname, mockFilePath),
+ {
+ injectDecorator: false,
+ parser: 'typescript',
+ }
+ );
+
+ it('does not inject stories decorator after the all "storiesOf" functions', () => {
+ expect(result.source).toMatchSnapshot();
+ });
+ });
+});
diff --git a/lib/source-loader/src/abstract-syntax-tree/parse-helpers.js b/lib/source-loader/src/abstract-syntax-tree/parse-helpers.js
new file mode 100644
index 000000000000..a96ed14b094c
--- /dev/null
+++ b/lib/source-loader/src/abstract-syntax-tree/parse-helpers.js
@@ -0,0 +1,159 @@
+const { toId } = require('@storybook/router/utils');
+
+const STORIES_OF = 'storiesOf';
+
+function pushParts(source, parts, from, to) {
+ const start = source.slice(from, to);
+ parts.push(start);
+
+ const end = source.slice(to);
+ parts.push(end);
+}
+
+function getKindFromStoryOfNode(object) {
+ if (object.arguments.length < 1) {
+ return '';
+ }
+
+ const kindArgument = object.arguments[0];
+
+ if (kindArgument.type === 'Literal' || kindArgument.type === 'StringLiteral') {
+ return kindArgument.value;
+ }
+
+ if (kindArgument.type === 'TemplateLiteral') {
+ // we can generate template, but it will not be a real value
+ // until the full template compilation. probably won't fix.
+ return '';
+ }
+
+ // other options may include some complex usages.
+ return '';
+}
+
+function findRelatedKind(object) {
+ if (!object || !object.callee) {
+ return '';
+ }
+
+ if (object.callee.name === STORIES_OF) {
+ return getKindFromStoryOfNode(object);
+ }
+
+ return findRelatedKind(object.callee.object);
+}
+
+export function patchNode(node) {
+ if (node.range && node.range.length === 2 && node.start === undefined && node.end === undefined) {
+ const [start, end] = node.range;
+
+ // eslint-disable-next-line no-param-reassign
+ node.start = start;
+ // eslint-disable-next-line no-param-reassign
+ node.end = end;
+ }
+
+ if (!node.range && node.start !== undefined && node.end !== undefined) {
+ // eslint-disable-next-line no-param-reassign
+ node.range = [node.start, node.end];
+ }
+
+ return node;
+}
+
+export function handleExportedName(kind, storyName, node) {
+ const startLoc = {
+ col: node.loc.start.column,
+ line: node.loc.start.line,
+ };
+ const endLoc = {
+ col: node.loc.end.column,
+ line: node.loc.end.line,
+ };
+ return {
+ [toId(kind, storyName)]: {
+ startLoc,
+ endLoc,
+ startBody: startLoc,
+ endBody: endLoc,
+ },
+ };
+}
+
+export function handleADD(node, parent, storiesOfIdentifiers) {
+ if (!node.property || !node.property.name || node.property.name.indexOf('add') !== 0) {
+ return {};
+ }
+
+ const addArgs = parent.arguments;
+
+ if (!addArgs || addArgs.length < 2) {
+ return {};
+ }
+
+ let tmp = node.object;
+
+ while (tmp.callee && tmp.callee.object) {
+ tmp = tmp.callee.object;
+ }
+
+ const framework = tmp.callee && tmp.callee.name && storiesOfIdentifiers[tmp.callee.name];
+
+ const storyName = addArgs[0];
+ const body = addArgs[1];
+ const lastArg = addArgs[addArgs.length - 1];
+
+ if (storyName.type !== 'Literal' && storyName.type !== 'StringLiteral') {
+ // if story name is not literal, it's much harder to extract it
+ return {};
+ }
+
+ const kind = findRelatedKind(node.object) || '';
+ if (kind && storyName.value) {
+ const key = toId(kind, storyName.value);
+ let idToFramework;
+ if (key && framework) {
+ idToFramework = { [key]: framework };
+ }
+
+ return {
+ toAdd: {
+ [key]: {
+ // Debug: code: source.slice(storyName.start, lastArg.end),
+ startLoc: {
+ col: storyName.loc.start.column,
+ line: storyName.loc.start.line,
+ },
+ endLoc: {
+ col: lastArg.loc.end.column,
+ line: lastArg.loc.end.line,
+ },
+ startBody: {
+ col: body.loc.start.column,
+ line: body.loc.start.line,
+ },
+ endBody: {
+ col: body.loc.end.column,
+ line: body.loc.end.line,
+ },
+ },
+ },
+ idToFramework,
+ };
+ }
+ return {};
+}
+
+export function handleSTORYOF(node, parts, source, lastIndex) {
+ if (!node.callee || !node.callee.name || node.callee.name !== STORIES_OF) {
+ return lastIndex;
+ }
+
+ parts.pop();
+ pushParts(source, parts, lastIndex, node.end);
+ return node.end;
+}
+
+export function asImport(node) {
+ return node.source.value;
+}
diff --git a/lib/source-loader/src/abstract-syntax-tree/parsers/index.js b/lib/source-loader/src/abstract-syntax-tree/parsers/index.js
new file mode 100644
index 000000000000..35a551ba0fb9
--- /dev/null
+++ b/lib/source-loader/src/abstract-syntax-tree/parsers/index.js
@@ -0,0 +1,20 @@
+function getParser(type) {
+ if (type === 'javascript' || /jsx?/.test(type) || !type) {
+ // eslint-disable-next-line global-require
+ return require('./parser-js').default;
+ }
+
+ if (type === 'typescript' || /tsx?/.test(type)) {
+ // eslint-disable-next-line global-require
+ return require('./parser-ts').default;
+ }
+
+ if (type === 'flow') {
+ // eslint-disable-next-line global-require
+ return require('./parser-flow').default;
+ }
+
+ throw new Error(`Parser of type "${type}" is not supported`);
+}
+
+export default getParser;
diff --git a/lib/source-loader/src/abstract-syntax-tree/parsers/parser-flow.js b/lib/source-loader/src/abstract-syntax-tree/parsers/parser-flow.js
new file mode 100644
index 000000000000..9414659af754
--- /dev/null
+++ b/lib/source-loader/src/abstract-syntax-tree/parsers/parser-flow.js
@@ -0,0 +1,13 @@
+import parseFlow from 'prettier/parser-flow';
+
+function parse(source) {
+ return parseFlow.parsers.flow.parse(source);
+}
+function format(source) {
+ return parseFlow.parsers.flow.format(source);
+}
+
+export default {
+ parse,
+ format,
+};
diff --git a/lib/source-loader/src/abstract-syntax-tree/parsers/parser-js.js b/lib/source-loader/src/abstract-syntax-tree/parsers/parser-js.js
new file mode 100644
index 000000000000..98c459aef1d1
--- /dev/null
+++ b/lib/source-loader/src/abstract-syntax-tree/parsers/parser-js.js
@@ -0,0 +1,21 @@
+import parseJs from 'prettier/parser-babylon';
+
+function parse(source) {
+ try {
+ return parseJs.parsers.babel.parse(source);
+ } catch (error1) {
+ try {
+ return JSON.stringify(source);
+ } catch (error) {
+ throw error1;
+ }
+ }
+}
+function format(source) {
+ return parseJs.parsers.babel.format(source);
+}
+
+export default {
+ parse,
+ format,
+};
diff --git a/lib/source-loader/src/abstract-syntax-tree/parsers/parser-ts.js b/lib/source-loader/src/abstract-syntax-tree/parsers/parser-ts.js
new file mode 100644
index 000000000000..ef4e369bb22a
--- /dev/null
+++ b/lib/source-loader/src/abstract-syntax-tree/parsers/parser-ts.js
@@ -0,0 +1,21 @@
+import parseTs from 'prettier/parser-typescript';
+
+function parse(source) {
+ try {
+ return parseTs.parsers.typescript.parse(source);
+ } catch (error1) {
+ try {
+ return JSON.stringify(source);
+ } catch (error) {
+ throw error1;
+ }
+ }
+}
+function format(source) {
+ return parseTs.parsers.typescript.format(source);
+}
+
+export default {
+ parse,
+ format,
+};
diff --git a/lib/source-loader/src/abstract-syntax-tree/traverse-helpers.js b/lib/source-loader/src/abstract-syntax-tree/traverse-helpers.js
new file mode 100644
index 000000000000..8dd3b1a2061f
--- /dev/null
+++ b/lib/source-loader/src/abstract-syntax-tree/traverse-helpers.js
@@ -0,0 +1,208 @@
+import { handleADD, handleSTORYOF, patchNode, handleExportedName } from './parse-helpers';
+
+const estraverse = require('estraverse');
+
+export function splitSTORYOF(ast, source) {
+ let lastIndex = 0;
+ const parts = [source];
+
+ estraverse.traverse(ast, {
+ fallback: 'iteration',
+ enter: node => {
+ patchNode(node);
+
+ if (node.type === 'CallExpression') {
+ lastIndex = handleSTORYOF(node, parts, source, lastIndex);
+ }
+ },
+ });
+
+ return parts;
+}
+export function splitExports(ast, source) {
+ const parts = [];
+ let lastIndex = 0;
+
+ estraverse.traverse(ast, {
+ fallback: 'iteration',
+ enter: node => {
+ patchNode(node);
+ if (
+ node.type === 'ExportNamedDeclaration' &&
+ node.declaration &&
+ node.declaration.declarations &&
+ node.declaration.declarations.length === 1 &&
+ node.declaration.declarations[0].type === 'VariableDeclarator' &&
+ node.declaration.declarations[0].id &&
+ node.declaration.declarations[0].id.name &&
+ node.declaration.declarations[0].init &&
+ ['CallExpression', 'ArrowFunctionExpression', 'FunctionExpression'].includes(
+ node.declaration.declarations[0].init.type
+ )
+ ) {
+ const functionNode = node.declaration.declarations[0].init;
+ parts.push(source.substring(lastIndex, functionNode.start - 1));
+ parts.push(source.substring(functionNode.start, functionNode.end));
+ lastIndex = functionNode.end;
+ }
+ },
+ });
+
+ if (source.length > lastIndex + 1) parts.push(source.substring(lastIndex + 1));
+ if (parts.length === 1) return [source];
+ return parts;
+}
+
+export function findAddsMap(ast, storiesOfIdentifiers) {
+ const addsMap = {};
+ const idsToFrameworks = {};
+
+ estraverse.traverse(ast, {
+ fallback: 'iteration',
+ enter: (node, parent) => {
+ patchNode(node);
+
+ if (node.type === 'MemberExpression') {
+ const { toAdd, idToFramework } = handleADD(node, parent, storiesOfIdentifiers);
+ Object.assign(addsMap, toAdd);
+ Object.assign(idsToFrameworks, idToFramework);
+ }
+ },
+ });
+
+ return { addsMap, idsToFrameworks };
+}
+
+// Handle cases like:
+// export const withText = () => ;
+// withText.title = 'with text';
+function findStoryTitle(storyVar, ast) {
+ const titleAssignment = ast.program.body.find(
+ d =>
+ d.type === 'ExpressionStatement' &&
+ d.expression &&
+ d.expression.type === 'AssignmentExpression' &&
+ d.expression.left &&
+ d.expression.left.object &&
+ d.expression.left.object.type === 'Identifier' &&
+ d.expression.left.object.name === storyVar &&
+ d.expression.right &&
+ d.expression.right.type === 'StringLiteral'
+ );
+ return titleAssignment && titleAssignment.expression.right.value;
+}
+
+export function findExportsMap(ast) {
+ const addsMap = {};
+ const idsToFrameworks = {};
+ const metaDeclaration =
+ ast &&
+ ast.program &&
+ ast.program.body &&
+ ast.program.body.find(
+ d =>
+ d.type === 'ExportDefaultDeclaration' &&
+ d.declaration.type === 'ObjectExpression' &&
+ (d.declaration.properties || []).length
+ );
+
+ const titleProperty =
+ metaDeclaration &&
+ metaDeclaration.declaration &&
+ metaDeclaration.declaration.properties.find(p => p.key && p.key.name === 'title');
+
+ if (!titleProperty) {
+ return { addsMap, idsToFrameworks };
+ }
+ const title = titleProperty.value.value;
+
+ estraverse.traverse(ast, {
+ fallback: 'iteration',
+ enter: node => {
+ patchNode(node);
+ if (
+ node.type === 'ExportNamedDeclaration' &&
+ node.declaration &&
+ node.declaration.declarations &&
+ node.declaration.declarations.length === 1 &&
+ node.declaration.declarations[0].type === 'VariableDeclarator' &&
+ node.declaration.declarations[0].id &&
+ node.declaration.declarations[0].id.name &&
+ node.declaration.declarations[0].init &&
+ ['CallExpression', 'ArrowFunctionExpression', 'FunctionExpression'].includes(
+ node.declaration.declarations[0].init.type
+ )
+ ) {
+ let storyName = node.declaration.declarations[0].id.name;
+ storyName = findStoryTitle(storyName, ast) || storyName;
+ const toAdd = handleExportedName(title, storyName, node.declaration.declarations[0].init);
+ Object.assign(addsMap, toAdd);
+ }
+ },
+ });
+ return { addsMap, idsToFrameworks };
+}
+
+export function findDependencies(ast) {
+ const dependencies = [];
+ const storiesOfIdentifiers = {};
+
+ estraverse.traverse(ast, {
+ fallback: 'iteration',
+ enter: node => {
+ patchNode(node);
+
+ if (node.type === 'ImportDeclaration') {
+ const candidateSpecifier = (node.specifiers || []).find(
+ s => (s.imported || {}).name === 'storiesOf'
+ );
+ if (node.source.value.startsWith('@storybook/') && candidateSpecifier) {
+ Object.assign(storiesOfIdentifiers, {
+ [candidateSpecifier.local.name]: node.source.value,
+ });
+ }
+ dependencies.push(node.source.value);
+ }
+ },
+ });
+ return { dependencies, storiesOfIdentifiers };
+}
+
+export function popParametersObjectFromDefaultExport(source, ast) {
+ let splicedSource = source;
+ let parametersSliceOfCode = '';
+ let indexWhereToAppend = -1;
+ let foundParametersProperty = false;
+ estraverse.traverse(ast, {
+ fallback: 'iteration',
+ enter: node => {
+ patchNode(node);
+
+ if (
+ node.type === 'ExportDefaultDeclaration' &&
+ node.declaration.type === 'ObjectExpression' &&
+ (node.declaration.properties || []).length
+ ) {
+ const parametersProperty = node.declaration.properties.find(
+ p => p.key.name === 'parameters' && p.value.type === 'ObjectExpression'
+ );
+
+ foundParametersProperty = !!parametersProperty;
+
+ splicedSource = parametersProperty
+ ? source.substring(0, parametersProperty.value.start) +
+ source.substring(parametersProperty.value.end + 1)
+ : splicedSource;
+
+ parametersSliceOfCode = parametersProperty
+ ? source.substring(parametersProperty.value.start, parametersProperty.value.end)
+ : '{}';
+
+ indexWhereToAppend = parametersProperty
+ ? parametersProperty.value.start
+ : node.declaration.start + 1;
+ }
+ },
+ });
+ return { splicedSource, parametersSliceOfCode, indexWhereToAppend, foundParametersProperty };
+}
diff --git a/lib/source-loader/src/build.js b/lib/source-loader/src/build.js
new file mode 100644
index 000000000000..685077cd4293
--- /dev/null
+++ b/lib/source-loader/src/build.js
@@ -0,0 +1,30 @@
+import { readStory } from './dependencies-lookup/readAsObject';
+import { getRidOfUselessFilePrefixes } from './dependencies-lookup/getRidOfUselessFilePrefixes';
+
+export function transform(inputSource) {
+ return readStory(this, inputSource)
+ .then(getRidOfUselessFilePrefixes)
+ .then(
+ ({
+ prefix,
+ resource,
+ source,
+ sourceJson,
+ addsMap,
+ dependencies,
+ localDependencies,
+ idsToFrameworks,
+ }) => `
+ var withSourceLoader = require('@storybook/source-loader').withSource;
+ var __SOURCE_PREFIX__ = "${prefix.replace(/\\([^\\ ])/g, '\\\\$1')}";
+ var __STORY__ = ${sourceJson};
+ var __ADDS_MAP__ = ${JSON.stringify(addsMap)};
+ var __MAIN_FILE_LOCATION__ = ${JSON.stringify(resource)};
+ var __MODULE_DEPENDENCIES__ = ${JSON.stringify(dependencies)};
+ var __LOCAL_DEPENDENCIES__ = ${JSON.stringify(localDependencies)};
+ var __IDS_TO_FRAMEWORKS__ = ${JSON.stringify(idsToFrameworks)};
+
+ ${source}
+ `
+ );
+}
diff --git a/lib/source-loader/src/dependencies-lookup/getRidOfUselessFilePrefixes.js b/lib/source-loader/src/dependencies-lookup/getRidOfUselessFilePrefixes.js
new file mode 100644
index 000000000000..1f0ca22ce8e1
--- /dev/null
+++ b/lib/source-loader/src/dependencies-lookup/getRidOfUselessFilePrefixes.js
@@ -0,0 +1,46 @@
+import path from 'path';
+
+function commonDir(...resources) {
+ const firstResource = (resources[0] || '').split(path.sep);
+ let i = 1;
+ while (
+ i < firstResource.length &&
+ // eslint-disable-next-line no-loop-func
+ resources.every(resource => resource.startsWith(firstResource.slice(0, i).join(path.sep)))
+ ) {
+ i += 1;
+ }
+ return firstResource.slice(0, i - 1).join(path.sep);
+}
+
+export function getRidOfUselessFilePrefixes({
+ resource,
+ source,
+ sourceJson,
+ addsMap,
+ dependencies,
+ localDependencies,
+ idsToFrameworks,
+}) {
+ const commondir = commonDir(resource, ...Object.keys(localDependencies || {}));
+ return {
+ prefix: commondir,
+ source,
+ sourceJson,
+ addsMap,
+ dependencies,
+ idsToFrameworks,
+ resource:
+ commondir === resource
+ ? '/index.js'
+ : resource.substring(commondir.length).replace(path.sep === '\\' ? /\\/g : /\//g, '/'),
+ localDependencies: Object.assign(
+ {},
+ ...Object.entries(localDependencies || {}).map(([depFileName, dependency]) => ({
+ [depFileName
+ .substring(commondir.length)
+ .replace(new RegExp(path.sep === '\\' ? /\\/g : /\//g, 'g'), '/')]: dependency,
+ }))
+ ),
+ };
+}
diff --git a/lib/source-loader/src/dependencies-lookup/readAsObject.js b/lib/source-loader/src/dependencies-lookup/readAsObject.js
new file mode 100644
index 000000000000..4ab9f83d8b8b
--- /dev/null
+++ b/lib/source-loader/src/dependencies-lookup/readAsObject.js
@@ -0,0 +1,119 @@
+import { getOptions } from 'loader-utils';
+import path from 'path';
+import injectDecorator from '../abstract-syntax-tree/inject-decorator';
+
+function extractDependenciesFrom(tree) {
+ return !Object.entries(tree || {}).length
+ ? []
+ : Object.entries(tree)
+ .map(([, value]) =>
+ (value.dependencies || []).concat(extractDependenciesFrom(value.localDependencies))
+ )
+ .reduce((acc, value) => acc.concat(value), []);
+}
+
+function extractLocalDependenciesFrom(tree) {
+ return Object.assign(
+ {},
+ ...Object.entries(tree || {}).map(([thisPath, value]) =>
+ Object.assign(
+ { [thisPath]: { code: value.source || value.code } },
+ extractLocalDependenciesFrom(value.localDependencies)
+ )
+ )
+ );
+}
+
+function readAsObject(classLoader, inputSource, mainFile) {
+ const options = getOptions(classLoader) || {};
+ const result = injectDecorator(
+ inputSource,
+ classLoader.resourcePath,
+ {
+ ...options,
+ parser: options.parser || classLoader.extension,
+ },
+ classLoader.emitWarning.bind(classLoader)
+ );
+
+ const sourceJson = JSON.stringify(result.storySource || inputSource)
+ .replace(/\u2028/g, '\\u2028')
+ .replace(/\u2029/g, '\\u2029');
+
+ const addsMap = result.addsMap || {};
+ const dependencies = result.dependencies || [];
+ const source = mainFile ? result.source : inputSource;
+ const idsToFrameworks = result.idsToFrameworks || {};
+ const resource = classLoader.resourcePath || classLoader.resource;
+
+ const moduleDependencies = (result.dependencies || []).filter(d => d[0] === '.' || d[0] === '/');
+ const workspaceFileNames = moduleDependencies.map(d => path.join(path.dirname(resource), d));
+
+ return Promise.all(
+ workspaceFileNames.map(
+ d =>
+ new Promise(resolve =>
+ classLoader.loadModule(d, (err1, compiledSource, sourceMap, theModule) => {
+ if (err1) {
+ classLoader.emitError(err1);
+ }
+ classLoader.fs.readFile(theModule.resource, (err2, dependencyInputData) => {
+ if (err2) {
+ classLoader.emitError(err2);
+ }
+ resolve({
+ d,
+ err: err1 || err2,
+ inputSource: dependencyInputData.toString(),
+ compiledSource,
+ sourceMap,
+ theModule,
+ });
+ });
+ })
+ )
+ )
+ )
+ .then(data =>
+ Promise.all(
+ data.map(({ inputSource: dependencyInputSource, theModule }) =>
+ readAsObject(
+ Object.assign({}, classLoader, {
+ resourcePath: theModule.resourcePath,
+ resource: theModule.resource,
+ extension: (theModule.resource || '').split('.').slice(-1)[0],
+ }),
+ dependencyInputSource
+ )
+ )
+ ).then(moduleObjects =>
+ Object.assign(
+ {},
+ ...moduleObjects.map(asObject => ({
+ [asObject.resource]: asObject,
+ }))
+ )
+ )
+ )
+ .then(localDependencies => ({
+ resource,
+ source,
+ sourceJson,
+ addsMap,
+ idsToFrameworks,
+ dependencies: dependencies
+ .concat(extractDependenciesFrom(localDependencies))
+ .filter(d => d[0] !== '.' && d[0] !== '/')
+ .map(d => (d[0] === '@' ? `${d.split('/')[0]}/${d.split('/')[1]}` : d.split('/')[0])),
+ localDependencies: Object.assign(
+ ...Object.entries(localDependencies).map(([name, value]) => ({
+ [name]: { code: value.source },
+ })),
+ extractLocalDependenciesFrom(localDependencies)
+ ),
+ }));
+}
+
+export function readStory(classLoader, inputSource) {
+ return readAsObject(classLoader, inputSource, true);
+}
diff --git a/lib/source-loader/src/events.js b/lib/source-loader/src/events.js
new file mode 100644
index 000000000000..ebeb7da51bde
--- /dev/null
+++ b/lib/source-loader/src/events.js
@@ -0,0 +1,2 @@
+export const ADDON_ID = 'storybook/source-loader';
+export const STORY_EVENT_ID = `${ADDON_ID}/set`;
diff --git a/lib/source-loader/src/index.js b/lib/source-loader/src/index.js
new file mode 100644
index 000000000000..bc39d9467511
--- /dev/null
+++ b/lib/source-loader/src/index.js
@@ -0,0 +1,6 @@
+import { transform } from './build';
+
+export { STORY_EVENT_ID } from './events';
+export { addSource, withSource } from './preview';
+
+export default transform;
diff --git a/lib/source-loader/src/preview.js b/lib/source-loader/src/preview.js
new file mode 100644
index 000000000000..55f2df746a25
--- /dev/null
+++ b/lib/source-loader/src/preview.js
@@ -0,0 +1,92 @@
+import addons from '@storybook/addons';
+import { STORY_EVENT_ID } from './events';
+
+const getLocation = (context, locationsMap) => locationsMap[context.id];
+
+function sendEvent(
+ context,
+ source,
+ locationsMap,
+ mainFileLocation,
+ dependencies,
+ localDependencies,
+ prefix,
+ idsToFrameworks
+) {
+ const channel = addons.getChannel();
+ const currentLocation = getLocation(context, locationsMap);
+
+ channel.emit(STORY_EVENT_ID, {
+ edition: {
+ source,
+ mainFileLocation,
+ dependencies,
+ localDependencies,
+ prefix,
+ idsToFrameworks,
+ },
+ story: {
+ kind: context.kind,
+ story: context.story,
+ },
+ location: {
+ currentLocation,
+ locationsMap,
+ },
+ });
+}
+
+export function addSource(story, sourceContext) {
+ const {
+ __STORY__: source,
+ __ADDS_MAP__: locationsMap = {},
+ __MAIN_FILE_LOCATION__: mainFileLocation = '/index.js',
+ __MODULE_DEPENDENCIES__: dependencies = [],
+ __LOCAL_DEPENDENCIES__: localDependencies = {},
+ __SOURCE_PREFIX__: prefix,
+ __IDS_TO_FRAMEWORKS__: idsToFrameworks,
+ } = sourceContext;
+ const decorated = function(context) {
+ sendEvent(
+ context,
+ source,
+ locationsMap,
+ mainFileLocation,
+ dependencies,
+ localDependencies,
+ prefix,
+ idsToFrameworks
+ );
+ if (typeof story === 'function') {
+ return story();
+ }
+ return story;
+ };
+ decorated.storyData = (story || {}).storyData;
+ decorated.title = (story || {}).title;
+ return decorated;
+}
+
+export function withSource(
+ source,
+ locationsMap = {},
+ mainFileLocation = '/index.js',
+ dependencies = [],
+ localDependencies = {},
+ prefix,
+ idsToFrameworks
+) {
+ return (story, context) => {
+ sendEvent(
+ context,
+ source,
+ locationsMap,
+ mainFileLocation,
+ dependencies,
+ localDependencies,
+ prefix,
+ idsToFrameworks
+ );
+ return story();
+ };
+}