├── .gitignore ├── .npmignore ├── after.png ├── before.png ├── webpack.config.js ├── package.json ├── src ├── index.js └── createFormatters.js ├── LICENSE ├── test.js ├── index.html └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | .DS_Store 5 | 6 | *.swp 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src/ 3 | 4 | .DS_Store 5 | 6 | *.swp 7 | -------------------------------------------------------------------------------- /after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewdavey/immutable-devtools/HEAD/after.png -------------------------------------------------------------------------------- /before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewdavey/immutable-devtools/HEAD/before.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./src/index.js", 3 | resolve: { 4 | extensions: ["", ".js"] 5 | }, 6 | output: { 7 | path: "./dist", 8 | filename: "index.js", 9 | library: "immutableDevTools", 10 | libraryTarget: "umd" 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: "babel-loader", 18 | query: { 19 | presets: ["es2015"] 20 | } 21 | } 22 | ] 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "immutable-devtools", 3 | "version": "0.1.5", 4 | "description": "Chrome Dev Tools formatter for the Immutable JS library", 5 | "main": "dist/index.js", 6 | "files": ["dist", "before.png", "after.png"], 7 | "scripts": { 8 | "build": "webpack --config ./webpack.config.js", 9 | "prepublish": "npm run-script build", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [ 13 | "immutable", 14 | "devtools", 15 | "chrome" 16 | ], 17 | "author": "Andrew Davey", 18 | "license": "BSD", 19 | "devDependencies": { 20 | "webpack": "^1.12.4", 21 | "babel-loader": "^6.1.0", 22 | "babel-core": "^6.1.4", 23 | "babel-preset-es2015": "^6.1.4", 24 | "immutable": "^4.0.0-rc.9", 25 | "immutable3": "^3.8.1" 26 | }, 27 | "repository" : { 28 | "type" : "git", 29 | "url" : "https://github.com/andrewdavey/immutable-devtools" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import createFormatters from './createFormatters'; 2 | 3 | // Check for globally defined Immutable and add an install method to it. 4 | if (typeof Immutable !== "undefined") { 5 | Immutable.installDevTools = install.bind(null, Immutable); 6 | } 7 | 8 | // I imagine most people are using Immutable as a CommonJS module though... 9 | 10 | let installed = false; 11 | function install(Immutable) { 12 | const gw = typeof window === "undefined" ? global : window; 13 | 14 | // Don't install more than once. 15 | if (installed === true) { 16 | return; 17 | } 18 | 19 | gw.devtoolsFormatters = gw.devtoolsFormatters || []; 20 | 21 | const { 22 | RecordFormatter, 23 | OrderedMapFormatter, 24 | OrderedSetFormatter, 25 | ListFormatter, 26 | MapFormatter, 27 | SetFormatter, 28 | StackFormatter 29 | } = createFormatters(Immutable); 30 | 31 | gw.devtoolsFormatters.push( 32 | RecordFormatter, 33 | OrderedMapFormatter, 34 | OrderedSetFormatter, 35 | ListFormatter, 36 | MapFormatter, 37 | SetFormatter, 38 | StackFormatter 39 | ); 40 | 41 | installed = true; 42 | } 43 | 44 | module.exports = install; 45 | export default install; 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Pavel Lang 2 | Copyright (c) 2015, Andrew Davey 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of immutable-devtools nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | // Run `node --inspect --inspect-brk test.js`, open the debugger, and check everything renders correctly 2 | 3 | var Immutable4 = require("immutable"); 4 | var Immutable3 = require("immutable3"); 5 | 6 | var installDevTools = require("./dist"); 7 | installDevTools(Immutable4); 8 | 9 | console.log("Testing with Immutable 4") 10 | runTests(Immutable4) 11 | console.log("Testing with Immutable 3") 12 | runTests(Immutable3) 13 | debugger 14 | 15 | function runTests(Immutable) { 16 | class ABRecord extends Immutable.Record({a:1,b:2}) { 17 | getAB() { 18 | return this.a + this.b; 19 | } 20 | } 21 | 22 | console.log("Record object itself, not a record instance - make sure there are no errors", ABRecord) 23 | 24 | var Xyz = new Immutable.Map({a: new Immutable.Map({b:8})}) 25 | console.log("Expand this and check child renders as a Map", Xyz) 26 | 27 | var Foo = Immutable.Record({bar: new Immutable.Map({a: 5}) }) 28 | var f = Foo() 29 | console.log("Expand this and check child renders as a Map", f) 30 | 31 | var record = new ABRecord(); 32 | var record2 = new ABRecord({a: 2}); 33 | console.log(record); 34 | console.log(record2); 35 | 36 | var orderedMap = Immutable.OrderedMap({key: "value"}); 37 | var orderedMap2 = Immutable.OrderedMap([["key", "value"], ["key2", "value2"]]); 38 | console.log(orderedMap); 39 | console.log(orderedMap2); 40 | 41 | var orderedSet = Immutable.OrderedSet(["hello", "aaa"]); 42 | console.log(orderedSet); 43 | 44 | var list = Immutable.List(["hello", "world"]); 45 | console.log(list) 46 | 47 | var map = Immutable.Map({hello: "world"}) 48 | console.log(map) 49 | 50 | var set = Immutable.Set(["hello", "aaa"]) 51 | console.log(set) 52 | 53 | var stack = Immutable.Stack(["hello", "aaa"]) 54 | console.log(stack) 55 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Immutable DevTools Demo 4 | 5 | 6 | 7 | 74 | 75 | 76 |

Immutable DevTools Demo

77 | 78 |

See 79 | README 80 | on project page 81 |

82 | 83 |

Open Console (F12) to see result of this code:

84 |

85 |     
89 |   
90 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # Chrome Dev Tools for Immutable-js
 2 | 
 3 | [![Npm Version](https://badge.fury.io/js/immutable-devtools.svg)](https://badge.fury.io/js/immutable-devtools)
 4 | [![NPM downloads](http://img.shields.io/npm/dm/immutable-devtools.svg)](https://www.npmjs.com/package/immutable-devtools)
 5 | [![devDependency Status](https://david-dm.org/andrewdavey/immutable-devtools/dev-status.svg)](https://david-dm.org/andrewdavey/immutable-devtools#info=devDependencies)
 6 | 
 7 | The [Immutable](http://facebook.github.io/immutable-js/) library is fantastic, but inspecting immutable collections in Chrome's Dev Tools is awkward. You only see the internal data structure, not the logical contents. For example, when inspecting the contents of an Immutable List, you'd really like to see the items in the list.
 8 | 
 9 | Chrome (v47+) has support for custom "formatters". A formatter tells Chrome's Dev Tools how to display values in the Console, Scope list, etc. This means we can display Lists, Maps and other collections, in a much better way.
10 | 
11 | Essentially, it turns this:
12 | 
13 | ![Before](before.png)
14 | 
15 | into:
16 | 
17 | ![After](after.png)
18 | 
19 | This library provides a formatter to do just that.
20 | 
21 | 
22 | ## Features
23 | 
24 | The library currently has formatters for:
25 | 
26 |  - [x] `List`
27 |  - [x] `Map` & `OrderedMap`
28 |  - [x] `Set` & `OrderedSet`
29 |  - [x] `Stack`
30 |  - [ ] `Range` (let me know if you use this :-), add :+1: to [#21](https://github.com/andrewdavey/immutable-devtools/issues/21))
31 |  - [ ] `Repeat` (if you wish this, add :+1: to [#22](https://github.com/andrewdavey/immutable-devtools/issues/22))
32 |  - [x] `Record`
33 |  - [ ] `Seq` — I do not have an idea how to display it. If you know, [write it down into #23](https://github.com/andrewdavey/immutable-devtools/issues/23)
34 | 
35 | Want something more? [Write down your wishes!](https://github.com/andrewdavey/immutable-devtools/issues/new)
36 | 
37 | ## Installation
38 | 
39 | Chrome v47+
40 | 
41 | In Dev Tools, press F1 to load the Settings. Scroll down to the Console section and tick "Enable custom formatters".
42 | 
43 | Then, in your project, install via npm:
44 | 
45 | ```
46 | npm install --save-dev immutable-devtools
47 | ```
48 | 
49 | And enable with:
50 | 
51 | ```js
52 | var Immutable = require("immutable");
53 | 
54 | var installDevTools = require("immutable-devtools");
55 | installDevTools(Immutable);
56 | ```
57 | 
58 | Note: You probably only want this library for debug builds, so perhaps wrap with `if (DEBUG) {...}` or similar.
59 | 
60 | ## Chrome Extension
61 | 
62 | Matt  Zeunert created a [Chrome Extension](https://github.com/mattzeunert/immutable-object-formatter-extension) based on this project. It automatically installs the formatters when you open the DevTools. ([Install from Chrome Web Store](https://chrome.google.com/webstore/detail/immutablejs-object-format/hgldghadipiblonfkkicmgcbbijnpeog)).
63 | 
64 | ## Using with webpack
65 | 
66 | You could use `webpack.DefinePlugin` to create a condition that will be allowed to install `immutable-devtools` in the debug build but unreachable in the production build:
67 | 
68 | ```javascript
69 | // webpack.config.js
70 | var webpack = require('webpack');
71 | module.exports = {
72 |   // ...
73 |   plugins: [
74 |     new webpack.DefinePlugin({
75 |       __DEV__: JSON.stringify(process.env.NODE_ENV !== 'production')
76 |     })
77 |   ],
78 | };
79 | ```
80 | 
81 | In your source you'd have something like this...
82 | 
83 | ```javascript
84 | // index.js
85 | var immutable = require('immutable');
86 | if (__DEV__) {
87 |   var installDevTools = require('immutable-devtools');
88 |   installDevTools(immutable);
89 | }
90 | ```
91 | 
92 | And then by adding the `-p` shortcut to webpack for production mode which enables dead code elimination, the condition that requires immutable-devtools will be removed.
93 | 
94 | ```
95 | NODE_ENV=production webpack -p index.js
96 | ```
97 | 
98 | See [#16](https://github.com/andrewdavey/immutable-devtools/issues/16) for more info.
99 | 


--------------------------------------------------------------------------------
/src/createFormatters.js:
--------------------------------------------------------------------------------
  1 | const listStyle = {style: 'list-style-type: none; padding: 0; margin: 0 0 0 12px; font-style: normal; position: relative'};
  2 | const immutableNameStyle = {style: 'color: rgb(232,98,0); position: relative'};
  3 | const keyStyle = {style: 'color: #881391'};
  4 | const defaultValueKeyStyle = {style: 'color: #777'};
  5 | const alteredValueKeyStyle = {style: 'color: #881391; font-weight: bolder'};
  6 | const inlineValuesStyle = {style: 'color: #777; font-style: italic; position: relative'}
  7 | const nullStyle = {style: 'color: #777'};
  8 | 
  9 | export default function createFormatter(Immutable) {
 10 | 
 11 |   const isRecord = maybeRecord => {
 12 |     if (maybeRecord &&
 13 |         maybeRecord._values === undefined // in v3 record
 14 |         && maybeRecord._map === undefined // in v4 record
 15 |       ) {
 16 |       // don't detect Immutable.Record.prototype as a Record instance
 17 |       return
 18 |     }
 19 |     // Immutable v4
 20 |     if (maybeRecord['@@__IMMUTABLE_RECORD__@@']) {
 21 |       // There's also a Immutable.Record.isRecord we could use, but then the
 22 |       // Immutable instance passed into createFormatter has to be the same
 23 |       // version as the one used to create the Immutable object.
 24 |       // That's especially a problem for the Chrome extension.
 25 |       return true
 26 |     }
 27 |     // Immutable v3
 28 |     return !!(
 29 |       maybeRecord['@@__IMMUTABLE_KEYED__@@'] && 
 30 |       maybeRecord['@@__IMMUTABLE_ITERABLE__@@'] &&
 31 |       maybeRecord._defaultValues !== undefined)
 32 |   }
 33 | 
 34 |   const reference = (object, config) => {
 35 |     if (typeof object === 'undefined')
 36 |       return ['span', nullStyle, 'undefined'];
 37 |     else if (object === 'null')
 38 |       return ['span', nullStyle, 'null'];
 39 | 
 40 |     return ['object', {object, config}];
 41 |   };
 42 | 
 43 |   const renderIterableHeader = (iterable, name = 'Iterable') =>
 44 |     ['span', ['span', immutableNameStyle, name], ['span', `[${iterable.size}]`]];
 45 | 
 46 | 
 47 |   const getKeySeq = collection => collection.toSeq().map((v, k) => k).toIndexedSeq()
 48 | 
 49 |   const hasBody = (collection, config) =>
 50 |     getKeySeq(collection).size > 0 && !(config && config.noPreview);
 51 | 
 52 |   const renderIterableBody = (collection, mapper, options = {}) => {
 53 |     if (options.sorted) {
 54 |       collection = collection.sortBy((value, key) => key);
 55 |     }
 56 |     const children = collection
 57 |       .map(mapper)
 58 |       .toList();
 59 | 
 60 |     const jsList = []
 61 |     // Can't just call toJS because that will also call toJS on children inside the list
 62 |     children.forEach(child => jsList.push(child))
 63 | 
 64 |     return [ 'ol', listStyle, ...children ];
 65 |   }
 66 | 
 67 |   const RecordFormatter = {
 68 |     header(record, config) {
 69 |       if (!(isRecord(record)))
 70 |         return null;
 71 | 
 72 |       const defaults = record.clear();
 73 |       const changed = !Immutable.is(defaults, record);
 74 | 
 75 |       if (config && config.noPreview)
 76 |         return ['span', changed ? immutableNameStyle : nullStyle,
 77 |           record._name || record.constructor.name || 'Record'];
 78 | 
 79 |       let inlinePreview;
 80 |       if (!changed) {
 81 |         inlinePreview = ['span', inlineValuesStyle, '{}'];
 82 |       } else {
 83 |         const preview = getKeySeq(record)
 84 |           .reduce((preview, key) => {
 85 |             if (Immutable.is(defaults.get(key), record.get(key)))
 86 |               return preview;
 87 |             if (preview.length)
 88 |               preview.push(', ');
 89 | 
 90 |             preview.push(['span', {},
 91 |               ['span', keyStyle, key + ': '],
 92 |               reference(record.get(key), {noPreview: true})
 93 |             ]);
 94 |             return preview;
 95 |           }, []);
 96 |         inlinePreview = ['span', inlineValuesStyle, '{', ...preview, '}'];
 97 |       }
 98 |       return ['span', {},
 99 |         ['span', immutableNameStyle, record._name || record.constructor.name || 'Record'],
100 |         ' ', inlinePreview
101 |       ];
102 |     },
103 |     hasBody,
104 |     body(record) {
105 |       const defaults = record.clear();
106 |       const children = getKeySeq(record)
107 |         .toJS()
108 |         .map(key => {
109 |           const style = Immutable.is(defaults.get(key), record.get(key))
110 |             ? defaultValueKeyStyle : alteredValueKeyStyle;
111 |           return [
112 |             'li', {},
113 |               ['span', style, key + ': '],
114 |               reference(record.get(key))
115 |             ]
116 |         });
117 |       return [ 'ol', listStyle, ...children ];
118 |     }
119 |   };
120 | 
121 |   const ListFormatter = {
122 |     header(o) {
123 |       if (!Immutable.List.isList(o))
124 |         return null;
125 |       return renderIterableHeader(o, 'List');
126 |     },
127 |     hasBody,
128 |     body(o) {
129 |       return renderIterableBody(o, (value, key) =>
130 |         ['li', ['span', keyStyle, key + ': '], reference(value)]
131 |       );
132 |     }
133 |   };
134 | 
135 |   const StackFormatter = {
136 |     header(o) {
137 |       if (!Immutable.Stack.isStack(o))
138 |         return null;
139 |       return renderIterableHeader(o, 'Stack');
140 |     },
141 |     hasBody,
142 |     body(o) {
143 |       return renderIterableBody(o, (value, key) =>
144 |         ['li', ['span', keyStyle, key + ': '], reference(value)]
145 |       );
146 |     }
147 |   };
148 | 
149 |   const MapFormatter = {
150 |     header(o) {
151 |       if (!Immutable.Map.isMap(o))
152 |         return null;
153 |       return renderIterableHeader(o, 'Map');
154 |     },
155 |     hasBody,
156 |     body(o) {
157 |       return renderIterableBody(o, (value, key) => 
158 |         ['li', {}, '{', reference(key), ' => ', reference(value), '}'],
159 |         {sorted: true}
160 |       );
161 |     }
162 |   };
163 | 
164 |   const OrderedMapFormatter = {
165 |     header(o) {
166 |       if (!Immutable.OrderedMap.isOrderedMap(o))
167 |         return null;
168 |       return renderIterableHeader(o, 'OrderedMap');
169 |     },
170 |     hasBody,
171 |     body(o) {
172 |       return renderIterableBody(o, (value, key) => 
173 |         ['li', {}, '{', reference(key), ' => ', reference(value), '}']
174 |       );
175 |     }
176 |   };
177 | 
178 |   const SetFormatter = {
179 |     header(o) {
180 |       if (!Immutable.Set.isSet(o))
181 |         return null;
182 |       return renderIterableHeader(o, 'Set');
183 |     },
184 |     hasBody,
185 |     body(o) {
186 |       return renderIterableBody(o, value =>
187 |         ['li', reference(value)],
188 |         {sorted: true}
189 |       );
190 |     }
191 |   };
192 | 
193 |   const OrderedSetFormatter = {
194 |     header(o) {
195 |       if (!Immutable.OrderedSet.isOrderedSet(o))
196 |         return null;
197 |       return renderIterableHeader(o, 'OrderedSet');
198 |     },
199 |     hasBody,
200 |     body(o) {
201 |       return renderIterableBody(o, value =>
202 |         ['li', reference(value)]
203 |       );
204 |     }
205 |   };
206 | 
207 |   return {
208 |     RecordFormatter,
209 |     OrderedMapFormatter,
210 |     OrderedSetFormatter,
211 |     ListFormatter,
212 |     MapFormatter,
213 |     SetFormatter,
214 |     StackFormatter
215 |   }
216 | }


--------------------------------------------------------------------------------