├── .babelrc ├── .gitignore ├── .npmignore ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── README.md ├── docs ├── favicon.ico ├── iframe.html ├── index.html └── static │ ├── manager.de735bcd2c3fb0a4f47d.bundle.js │ ├── manager.de735bcd2c3fb0a4f47d.bundle.js.map │ ├── preview.1ea4d830b6a88a003a76.bundle.js │ ├── preview.1ea4d830b6a88a003a76.bundle.js.map │ ├── runtime~manager.4df74dbc3c7ba7bb0154.bundle.js │ ├── runtime~manager.4df74dbc3c7ba7bb0154.bundle.js.map │ ├── runtime~preview.54cc4290e870f934d9e1.bundle.js │ ├── runtime~preview.54cc4290e870f934d9e1.bundle.js.map │ ├── vendors~preview.d670255e5790e10518d6.bundle.js │ └── vendors~preview.d670255e5790e10518d6.bundle.js.map ├── package-lock.json ├── package.json ├── src └── DagreD3.jsx ├── stories ├── DynamicGraph.jsx ├── graphs.jsx ├── index.jsx └── styles.scss └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react", 5 | "stage-0", 6 | ] 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | /node_modules 4 | /build 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | docs/ 3 | stories/ 4 | .storybook/ 5 | .idea/ 6 | .babelrc 7 | .gitignore 8 | *.iml 9 | webpack.config.js 10 | README.md -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-options/register' 2 | import '@storybook/addon-actions/register' 3 | import '@storybook/addon-knobs/register' -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | import { setOptions } from '@storybook/addon-options' 3 | 4 | setOptions({ 5 | name: 'REACT-DAGRE-D3 GITHUB', 6 | url: 'https://github.com/arxenix/React-DagreD3', 7 | goFullScreen: false, 8 | showLeftPanel: true, 9 | showDownPanel: true, 10 | showSearchBox: false, 11 | downPanelInRight: true 12 | }); 13 | 14 | function loadStories() { 15 | require('../stories/index.jsx'); 16 | } 17 | 18 | configure(loadStories, module); 19 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | resolve: { 5 | extensions: ['.js', '.jsx'] 6 | }, 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.jsx?$/, 11 | exclude: /(node_modules|bower_components|build)/, 12 | use: { 13 | loader: 'babel-loader' 14 | } 15 | }, 16 | { 17 | test: /\.scss$/, 18 | loaders: ["style-loader", "css-loader", "sass-loader"], 19 | } 20 | ] 21 | } 22 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-dagre-d3 2 | [React](https://reactjs.org/) component for [Dagre-D3](https://github.com/dagrejs/dagre-d3) 3 | 4 | 5 | 6 | [Demo](https://arxenix.github.io/react-dagre-d3/) 7 | 8 | # Usage 9 | Download via [NPM](https://www.npmjs.com/package/react-dagre-d3) 10 | `npm install react-dagre-d3` 11 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arxenix/react-dagre-d3/7fb5d75deda3d91f7a84da1deccd06bb9f75032a/docs/favicon.ico -------------------------------------------------------------------------------- /docs/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Storybook 9 | 10 | 11 | 67 | 68 | 69 |
70 | 71 |
72 |
73 |

No Preview

74 |

Sorry, but you either have no stories or none are selected somehow.

75 | 79 |
80 |
81 | 82 |
83 |
84 |
85 |             
86 |         
87 |
88 | 89 | 90 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Storybook 9 | 10 | 11 | 12 | 13 | 14 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/static/preview.1ea4d830b6a88a003a76.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{626:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){return function(e,t){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return function(e,t){var n=[],r=!0,o=!1,a=void 0;try{for(var d,i=e[Symbol.iterator]();!(r=(d=i.next()).done)&&(n.push(d.value),!t||n.length!==t);r=!0);}catch(e){o=!0,a=e}finally{try{!r&&i.return&&i.return()}finally{if(o)throw a}}return n}(e,t);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),o=function(){function e(e,t){for(var n=0;n inner.attr(\"transform\", d3.event.transform));\n svg.call(zoom);\n }\n\n // Create the renderer\n let render = new dagreD3.render();\n\n // set up custom shape renderers\n if (this.props.shapeRenderers)\n for (let [shape, renderer] of Object.entries(this.props.shapeRenderers))\n render.shapes()[shape] = renderer;\n\n // Run the renderer. This is what draws the final graph.\n render(inner, g);\n\n\n // TODO add padding?\n if (this.props.fit) {\n let {height: gHeight, width: gWidth} = g.graph();\n let {height, width} = this.nodeTree.getBBox();\n let transX = width - gWidth;\n let transY = height - gHeight;\n svg.attr(\"height\", height);\n svg.attr(\"width\", width);\n inner.attr(\"transform\", d3.zoomIdentity.translate(transX, transY))\n }\n\n if (this.props.onNodeClick)\n svg.selectAll('.dagre-d3 .node').on('click',\n id => this.props.onNodeClick(id));\n }\n\n render() {\n return (\n {this.nodeTree = r}}\n width={this.props.height}\n height={this.props.width}>\n\n {this.nodeTreeGroup = r}}/>\n \n );\n }\n}\n\nexport default DagreD3;"],"mappings":"AAAA","sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/runtime~manager.4df74dbc3c7ba7bb0154.bundle.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c (https://github.com/arxenix)", 19 | "main": "build/index.js", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/arxenix/react-dagre-d3.git" 23 | }, 24 | "scripts": { 25 | "build": "webpack -p", 26 | "build:dev": "webpack -d", 27 | "build:storybook": "build-storybook -c .storybook -o docs", 28 | "prepublishOnly": "npm run build", 29 | "publish": "npm publish . --access public", 30 | "storybook": "start-storybook -p 9001 -c .storybook" 31 | }, 32 | "peerDependencies": { 33 | "react": "^16.4.1", 34 | "react-dom": "^16.4.1" 35 | }, 36 | "dependencies": { 37 | "dagre-d3": "^0.6.1", 38 | "prop-types": "^15.6.2", 39 | "react-fast-compare": "^2.0.1" 40 | }, 41 | "devDependencies": { 42 | "@storybook/addon-actions": "^4.0.0-alpha.14", 43 | "@storybook/addon-info": "^4.0.0-alpha.14", 44 | "@storybook/addon-knobs": "^4.0.0-alpha.14", 45 | "@storybook/addon-options": "^4.0.0-alpha.14", 46 | "@storybook/react": "^4.0.0-alpha.14", 47 | "babel-core": "^6.26.3", 48 | "babel-loader": "^7.1.5", 49 | "babel-plugin-lodash": "^3.3.4", 50 | "babel-preset-env": "^1.7.0", 51 | "babel-preset-react": "^6.24.1", 52 | "babel-preset-stage-0": "^6.24.1", 53 | "lodash-webpack-plugin": "^0.11.5", 54 | "moment": "^2.22.2", 55 | "node-sass": "^4.9.2", 56 | "react": "^16.4.1", 57 | "react-dom": "^16.4.1", 58 | "sass-loader": "^7.0.3", 59 | "webpack": "^4.16.2", 60 | "webpack-bundle-analyzer": "^2.13.1", 61 | "webpack-cli": "^3.1.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/DagreD3.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import * as dagreD3 from 'dagre-d3' 4 | import * as d3 from 'd3' 5 | 6 | import isEqual from 'react-fast-compare' 7 | 8 | class DagreD3 extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | static defaultProps = { 14 | height: "1", 15 | width: "1", 16 | // width and height are defaulted to 1 due to a FireFox bug(?) If set to 0, it complains. 17 | fit: true, 18 | interactive: false 19 | }; 20 | 21 | static propTypes = { 22 | nodes: PropTypes.object.isRequired, 23 | edges: PropTypes.array.isRequired, 24 | interactive: PropTypes.bool, 25 | fit: PropTypes.bool, 26 | height: PropTypes.string, 27 | width: PropTypes.string, 28 | shapeRenderers: PropTypes.objectOf(PropTypes.func), 29 | onNodeClick: PropTypes.func, 30 | }; 31 | 32 | shouldComponentUpdate(nextProps, nextState) { 33 | return !isEqual(this.props.nodes, nextProps.nodes) || 34 | !isEqual(this.props.edges, nextProps.edges) || 35 | !isEqual(this.props.zoom, nextProps.zoom) 36 | } 37 | 38 | componentDidMount() { 39 | this.renderDag(); 40 | } 41 | 42 | componentDidUpdate() { 43 | this.renderDag(); 44 | } 45 | 46 | renderDag() { 47 | let g = new dagreD3.graphlib.Graph().setGraph({}); 48 | 49 | 50 | for (let [id, node] of Object.entries(this.props.nodes)) 51 | g.setNode(id, node); 52 | 53 | for (let edge of this.props.edges) 54 | g.setEdge(edge[0], edge[1], edge[2]); // from, to, props 55 | 56 | // Set up an SVG group so that we can translate the final graph. 57 | let svg = d3.select(this.nodeTree); 58 | let inner = d3.select(this.nodeTreeGroup); 59 | 60 | // set up zoom support 61 | if (this.props.interactive) { 62 | let zoom = d3.zoom().on("zoom", 63 | () => inner.attr("transform", d3.event.transform)); 64 | svg.call(zoom); 65 | } 66 | 67 | // Create the renderer 68 | let render = new dagreD3.render(); 69 | 70 | // set up custom shape renderers 71 | if (this.props.shapeRenderers) 72 | for (let [shape, renderer] of Object.entries(this.props.shapeRenderers)) 73 | render.shapes()[shape] = renderer; 74 | 75 | // Run the renderer. This is what draws the final graph. 76 | render(inner, g); 77 | 78 | 79 | // TODO add padding? 80 | if (this.props.fit) { 81 | let {height: gHeight, width: gWidth} = g.graph(); 82 | let {height, width} = this.nodeTree.getBBox(); 83 | let transX = width - gWidth; 84 | let transY = height - gHeight; 85 | svg.attr("height", height); 86 | svg.attr("width", width); 87 | inner.attr("transform", d3.zoomIdentity.translate(transX, transY)) 88 | } 89 | 90 | if (this.props.onNodeClick) 91 | svg.selectAll('.dagre-d3 .node').on('click', 92 | id => this.props.onNodeClick(id)); 93 | } 94 | 95 | render() { 96 | return ( 97 | {this.nodeTree = r}} 98 | width={this.props.height} 99 | height={this.props.width}> 100 | 101 | {this.nodeTreeGroup = r}}/> 102 | 103 | ); 104 | } 105 | } 106 | 107 | export default DagreD3; -------------------------------------------------------------------------------- /stories/DynamicGraph.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DagreD3 from '../src/DagreD3.jsx'; 3 | 4 | class DynamicGraph1 extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | nodes: {}, 9 | edges: [], 10 | index: 0 11 | }; 12 | } 13 | 14 | componentDidMount() { 15 | this.update(); 16 | this.timerID = setInterval( 17 | () => this.update(), 18 | 1500 19 | ); 20 | } 21 | 22 | componentWillUnmount() { 23 | clearInterval(this.timerID); 24 | } 25 | 26 | update() { 27 | let nodes = {}; 28 | let edges = []; 29 | nodes['input'] = {}; 30 | nodes['output'] = {}; 31 | 32 | for (let i = 0; i < this.state.index + 1; i++) { 33 | nodes[`${i}`] = { 34 | label: `Task ${i}` 35 | }; 36 | edges.push(['input', `${i}`, {}]); 37 | edges.push([`${i}`, 'output', {}]); 38 | } 39 | 40 | this.setState({nodes: nodes, edges: edges, index: (this.state.index + 1) % 5}); 41 | } 42 | 43 | render() { 44 | return 45 | } 46 | } 47 | 48 | class DynamicGraph2 extends React.Component { 49 | constructor(props) { 50 | super(props); 51 | this.state = { 52 | nodes: {}, 53 | edges: [], 54 | index: 0 55 | }; 56 | } 57 | 58 | componentDidMount() { 59 | this.update(); 60 | this.timerID = setInterval( 61 | () => this.update(), 62 | 10 63 | ); 64 | } 65 | 66 | componentWillUnmount() { 67 | clearInterval(this.timerID); 68 | } 69 | 70 | RGB2Color(r,g,b) { 71 | return 'rgb(' + Math.round(r) + ',' + Math.round(g) + ',' + Math.round(b) + ')'; 72 | } 73 | 74 | update() { 75 | let nodes = {}; 76 | let edges = []; 77 | nodes['input'] = {}; 78 | nodes['output'] = {}; 79 | 80 | 81 | edges.push(['input', 'color', {}]); 82 | edges.push(['color', 'output', {}]); 83 | 84 | let center = 128; 85 | let width = 127; 86 | let frequency = Math.PI*2/360; 87 | let red = Math.sin(frequency*this.state.index+2) * width + center; 88 | let green = Math.sin(frequency*this.state.index+0) * width + center; 89 | let blue = Math.sin(frequency*this.state.index+4) * width + center; 90 | nodes['color'] = { 91 | label: 'taste the rainbow', 92 | style: `fill: ${this.RGB2Color(red, green, blue)};` 93 | }; 94 | this.setState({nodes: nodes, edges: edges, index: (this.state.index + 1) % 360}) 95 | } 96 | 97 | render() { 98 | return 99 | } 100 | } 101 | 102 | export {DynamicGraph1, DynamicGraph2} ; -------------------------------------------------------------------------------- /stories/graphs.jsx: -------------------------------------------------------------------------------- 1 | let graphs = { 2 | simple: { 3 | nodes: { 4 | '1': { 5 | label: 'Node 1' 6 | }, 7 | '2': { 8 | label: 'Node 2' 9 | }, 10 | '3': { 11 | label: 'Node 3' 12 | }, 13 | '4': { 14 | label: 'Node 4' 15 | } 16 | }, 17 | edges: [ 18 | ['1', '2', {}], 19 | ['1', '3', {}], 20 | ['2', '4', {}], 21 | ['3', '4', {}] 22 | ] 23 | }, 24 | medium: { 25 | nodes: { 26 | '1': {}, '2': {}, '3': {}, '4': {}, '5': {}, 27 | '6': {}, 28 | }, 29 | edges: [ 30 | ['1', '2', {}], 31 | ['1', '3', {}], 32 | ['2', '4', {}], 33 | ['3', '4', {}], 34 | ['4', '5', {}], 35 | ['1', '6', {}], 36 | ['5', '6', {}] 37 | ] 38 | }, 39 | large: { 40 | nodes: { 41 | '1': {}, '2': {}, '3': {}, '4': {}, '5': {}, '6': {}, 42 | '7': {}, '8': {}, '9': {}, '10': {}, '11': {}, '12': {} 43 | }, 44 | edges: [ 45 | ['1', '2', {}], 46 | ['1', '5', {}], 47 | ['2', '3', {}], 48 | ['3', '4', {}], 49 | ['5', '6', {}], 50 | ['5', '7', {}], 51 | ['5', '8', {}], 52 | ['6', '9', {}], 53 | ['7', '10', {}], 54 | ['2', '9', {}], 55 | ['2', '10', {}], 56 | ['9', '11', {}], 57 | ['10', '11', {}], 58 | ['11', '12', {}] 59 | ] 60 | } 61 | }; 62 | 63 | export default graphs; 64 | -------------------------------------------------------------------------------- /stories/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | import {action} from '@storybook/addon-actions'; 4 | import {withInfo} from '@storybook/addon-info'; 5 | import DagreD3 from '../src/DagreD3.jsx'; 6 | import * as dagreD3 from 'dagre-d3'; 7 | import * as d3 from 'd3'; 8 | import {DynamicGraph1, DynamicGraph2} from "./DynamicGraph.jsx"; 9 | import graphs from './graphs.jsx'; 10 | import './styles.scss'; 11 | 12 | 13 | 14 | storiesOf('Basic Settings', module) 15 | .add('basic graph', 16 | withInfo(` 17 | A simple 4 node graph 18 | `)(() => { 19 | return 20 | })) 21 | .add('svg size', withInfo(` 22 | Setting the size of the svg 23 | `)(() => { 24 | return 26 | })) 27 | .add('interactive', withInfo(` 28 | Make the graph interactive (zoomable and draggable) 29 | `)(() => { 30 | return 32 | })) 33 | .add('another graph', withInfo(` 34 | A slightly more complex graph 35 | `)(() => { 36 | return 37 | })) 38 | .add('complex graph', withInfo(` 39 | A more complex graph 40 | `)(() => { 41 | return 42 | })) 43 | .add('dynamic graph 1', withInfo(` 44 | A dynamic graph whose nodes update and it redraws 45 | `)(() => { 46 | return 47 | })) 48 | .add('dynamic graph 2', withInfo(` 49 | A dynamic graph whose node style updates and it redraws 50 | `)(() => { 51 | return 52 | })); 53 | 54 | storiesOf('Node Settings', module) 55 | .add('click events', withInfo(` 56 | Click handlers on nodes 57 | `)(() => { 58 | return 59 | })) 60 | .add('node classes', withInfo(` 61 | Attach classes to specific nodes 62 | `)(() => { 63 | let classes = JSON.parse(JSON.stringify(graphs.simple)); 64 | classes.nodes['1'].class = 'red'; 65 | classes.nodes['2'].class = 'green'; 66 | return 67 | })) 68 | .add('node shapes', withInfo(` 69 | Set custom node shapes (rect, ellipse, circle, diamond) 70 | `)(() => { 71 | let shapes = JSON.parse(JSON.stringify(graphs.simple)); 72 | shapes.nodes['1'].shape = 'circle'; 73 | shapes.nodes['2'].shape = 'diamond'; 74 | shapes.nodes['3'].shape = 'ellipse'; 75 | shapes.nodes['4'].shape = 'rect'; 76 | return 77 | })) 78 | .add('combined example', withInfo(` 79 | Example showing all of the above 80 | `)(() => { 81 | let combined = JSON.parse(JSON.stringify(graphs.simple)); 82 | combined.nodes['1'].class = 'clickable red'; 83 | combined.nodes['1'].shape = 'circle'; 84 | combined.nodes['4'].class = 'clickable red'; 85 | combined.nodes['4'].shape = 'circle'; 86 | return 87 | })) 88 | .add('custom shape renderer', withInfo(` 89 | A custom shape renderer 90 | `)(() => { 91 | let house = (parent, bbox, node) => { 92 | const w = bbox.width, h = bbox.height; 93 | const points = [ 94 | {x: 0, y: 0}, 95 | {x: w, y: 0}, 96 | {x: w, y: -h}, 97 | {x: w / 2, y: -h * 3 / 2}, 98 | {x: 0, y: -h} 99 | ]; 100 | let shapeSvg = parent.insert("polygon", ":first-child") 101 | .attr("points", points.map(function (d) { 102 | return d.x + "," + d.y; 103 | }).join(" ")) 104 | .attr("transform", "translate(" + (-w / 2) + "," + (h * 3 / 4) + ")"); 105 | 106 | node.intersect = (point) => dagreD3.intersect.polygon(node, points, point); 107 | return shapeSvg; 108 | }; 109 | let combined = JSON.parse(JSON.stringify(graphs.simple)); 110 | combined.nodes['2'].shape = 'house'; 111 | return 112 | })); 113 | 114 | storiesOf('Edge Settings', module) 115 | .add('edge labels', withInfo(` 116 | Attaching labels to edges 117 | `)(() => { 118 | let labels = JSON.parse(JSON.stringify(graphs.simple)); 119 | labels.edges[1][2].label = 'edge 1'; 120 | labels.edges[2][2].label = 'edge 2'; 121 | return 122 | })) 123 | .add('edge classes', withInfo(` 124 | Attaching classes to specific edges 125 | `)(() => { 126 | let curve = JSON.parse(JSON.stringify(graphs.simple)); 127 | curve.edges[1][2].class = 'dashed'; 128 | curve.edges[2][2].class = 'dashed'; 129 | return 130 | })) 131 | .add('custom edge renderer', withInfo(` 132 | An example of using d3's default curveBasis renderer 133 | `)(() => { 134 | let curve = JSON.parse(JSON.stringify(graphs.simple)); 135 | for (let edge of curve.edges) { 136 | edge[2].curve = d3.curveBasis; 137 | } 138 | return 139 | })); 140 | -------------------------------------------------------------------------------- /stories/styles.scss: -------------------------------------------------------------------------------- 1 | $shapes: "rect, polygon, circle, ellipse, diamond"; 2 | // BASIC STYLING 3 | .dagre-d3 { 4 | //styles for graph nodes 5 | .node { 6 | fill: white; 7 | 8 | #{$shapes} { 9 | stroke: black; 10 | stroke-width: 1px; 11 | } 12 | 13 | .label { 14 | fill: black; 15 | } 16 | 17 | text { 18 | font: 300 14px 'Helvetica Neue', Helvetica; 19 | } 20 | } 21 | //styles for graph edges 22 | .edgePath { 23 | path.path { 24 | stroke: black; 25 | fill: black; 26 | stroke-width: 1.5px; 27 | } 28 | } 29 | 30 | .edgeLabel text { 31 | font: 300 14px 'Helvetica Neue', Helvetica; 32 | } 33 | } 34 | 35 | 36 | // custom styles for demo 37 | .dagre-d3 { 38 | border: 1px dashed grey; 39 | 40 | .node { 41 | &.clickable { 42 | #{$shapes} { 43 | cursor: pointer; 44 | } 45 | text { 46 | cursor: pointer; 47 | user-select: none; 48 | } 49 | &:hover { 50 | #{$shapes} { 51 | stroke-width: 3.5px; 52 | } 53 | } 54 | } 55 | &.red { 56 | #{$shapes} { 57 | fill: red; 58 | } 59 | } 60 | &.green { 61 | #{$shapes} { 62 | fill: green; 63 | } 64 | } 65 | } 66 | 67 | .edgePath { 68 | &.dashed { 69 | stroke-dasharray: 5, 5; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/DagreD3.jsx', 5 | output: { 6 | path: path.resolve(__dirname, 'build'), 7 | filename: 'index.js', 8 | }, 9 | resolve: { 10 | extensions: ['.js', '.jsx'] 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.jsx?$/, 16 | include: path.resolve(__dirname, 'src'), 17 | exclude: /(node_modules|bower_components|build)/, 18 | use: { 19 | loader: 'babel-loader' 20 | } 21 | }, 22 | { 23 | test: /\.scss$/, 24 | loaders: ["style-loader", "css-loader", "sass-loader"], 25 | } 26 | ] 27 | }, 28 | externals: { 29 | 'react': 'commonjs react' // this line is just to use the React dependency of our parent-testing-project instead of using our own React. 30 | } 31 | }; --------------------------------------------------------------------------------