├── lib ├── client │ ├── styles.less │ ├── client.js │ ├── index.pug │ ├── Components │ │ ├── LogWarning.jsx │ │ ├── Log.jsx │ │ ├── Plugin.jsx │ │ ├── PluginList.jsx │ │ └── TreeByPlugin.jsx │ ├── App.jsx │ └── index.html └── index.js ├── .gitignore ├── .npmignore ├── webpack.config.js ├── .README.md ├── README.md └── package.json /lib/client/styles.less: -------------------------------------------------------------------------------- 1 | .margin-top { 2 | margin-top: 15px; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/fixtures/*/build 3 | .nyc_output 4 | coverage 5 | docs 6 | config/local.js 7 | npm-debug.log 8 | images 9 | 10 | dist 11 | cache 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/fixtures/*/build 3 | .nyc_output 4 | coverage 5 | docs 6 | config/local.js 7 | npm-debug.log 8 | images 9 | 10 | .README.md 11 | lib 12 | test 13 | cache 14 | -------------------------------------------------------------------------------- /lib/client/client.js: -------------------------------------------------------------------------------- 1 | import { render } from 'react-dom' 2 | import App from './App' 3 | import { 4 | default as React 5 | } from 'react' 6 | 7 | const root = ( 8 | 9 | ) 10 | render(root, document.getElementById('root')) 11 | -------------------------------------------------------------------------------- /lib/client/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset="utf-8") 5 | meta(name="viewport", content="width=device-width, initial-scale=1.0") 6 | meta(http-equiv="X-UA-Compatible" content="IE=edge") 7 | meta(name="description" content="Metalsmith Debug UI") 8 | 9 | title= 'Metalsmith Debug UI' 10 | 11 | link(href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css", rel="stylesheet") 12 | link(href='styles.css', rel='stylesheet') 13 | 14 | body 15 | div#root.container 16 | script(src="client.js") 17 | -------------------------------------------------------------------------------- /lib/client/Components/LogWarning.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | default as React // eslint-disable-line 4 | } from 'react' 5 | import cookie from 'react-cookie' 6 | import { 7 | Alert, 8 | Button 9 | } from 'react-bootstrap' 10 | 11 | export default class LogWarning extends Component { 12 | constructor (props) { 13 | super(props) 14 | this.state = { 15 | alertVisible: !cookie.load('debugUiHideWarning') 16 | } 17 | } 18 | render () { 19 | if (!this.state.alertVisible) return null 20 | return ( 21 | 22 |

Not logging all the things?

23 |

24 | It's possible that a plugin's output may not be captured here. If you 25 | find one please log an issue about 26 | it! 27 |

28 |

29 | 30 |

31 |
32 | ) 33 | } 34 | handleAlertDismiss () { 35 | cookie.save('debugUiHideWarning', true, { 36 | path: '/', 37 | expires: new Date(Date.now() + (30 * 24 * 60 * 60 * 1000)) 38 | }) 39 | this.setState({alertVisible: false}) 40 | } 41 | handleAlertShow () { 42 | this.setState({alertVisible: true}) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/client/Components/Log.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | default as React // eslint-disable-line 4 | } from 'react' 5 | import PropTypes from 'prop-types' 6 | import Table from 'react-filterable-table' 7 | import LogWarning from './LogWarning' 8 | 9 | 10 | import styles from 'react-filterable-table/dist/style.css' 11 | 12 | export default class Log extends Component { 13 | render () { 14 | let table 15 | if (this.props.log.length) { 16 | table = ( 17 | 30 | ) 31 | } else { 32 | table = ( 33 |

No logs yet

34 | ) 35 | } 36 | return ( 37 |
38 |
39 | 40 |
41 |
42 | {table} 43 |
44 |
45 | ) 46 | } 47 | } 48 | 49 | Log.propTypes = { 50 | log: PropTypes.array.isRequired 51 | } 52 | -------------------------------------------------------------------------------- /lib/client/Components/Plugin.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | default as React // eslint-disable-line 4 | } from 'react' 5 | // import update from 'immutability-helper' 6 | import PropTypes from 'prop-types' 7 | import { 8 | Button 9 | } from 'react-bootstrap' 10 | 11 | export default class Plugin extends Component { 12 | constructor (props) { 13 | super(props) 14 | this.onClick = this.onClick.bind(this) 15 | } 16 | // updateState (...args) { 17 | // const callback = args.pop() 18 | // this.setstate(update(...args), callback) 19 | // } 20 | onClick () { 21 | this.props.setActivePlugin(this.props.idx) 22 | } 23 | render () { 24 | return ( 25 | 31 | ) 32 | // if (this.props.appearances < 2 && !this.props.showSingles) return null 33 | // return ( 34 | // 41 | // ) 42 | } 43 | } 44 | Plugin.propTypes = { 45 | fnName: PropTypes.string.isRequired, 46 | isActive: PropTypes.bool.isRequired, 47 | idx: PropTypes.number.isRequired, 48 | setActivePlugin: PropTypes.func.isRequired 49 | } 50 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const baseDir = __dirname 4 | const prodn = process.env.NODE_ENV === 'production' 5 | console.log(`webpack ${prodn ? 'production' : 'development'} build`) 6 | process.traceDeprecation = true 7 | 8 | const config = { 9 | devtool: 'source-map', 10 | entry: path.resolve(baseDir, 'lib', 'client', 'client.js'), 11 | output: { 12 | path: path.resolve(baseDir, 'dist', 'client'), 13 | filename: 'client.js' 14 | }, 15 | resolve: { 16 | extensions: ['.js', '.jsx'], 17 | alias: {} 18 | }, 19 | plugins: [ 20 | // prodn ? new webpack.ProvidePlugin({ 21 | // React: 'react' // doesn't work 22 | // }) : false, 23 | prodn ? new webpack.optimize.UglifyJsPlugin({ 24 | sourceMap: !prodn, 25 | compressor: { 26 | warnings: false 27 | } 28 | }) : false 29 | ].filter((e) => e), 30 | module: { 31 | loaders: [ 32 | { 33 | test: /\.(js|jsx)$/, 34 | include: [ 35 | path.resolve(baseDir, 'lib'), 36 | path.resolve(baseDir, 'lib', 'client'), 37 | path.resolve(baseDir, 'lib', 'client', 'Components') 38 | ], 39 | exclude: /node_modules/, 40 | use: { 41 | loader: 'babel-loader', 42 | options: { 43 | presets: [ 44 | 'es2015', 45 | 'react' 46 | ] 47 | } 48 | } 49 | }, 50 | { 51 | test: /\.css$/, 52 | use: [ 53 | { loader: 'style-loader' }, 54 | { loader: 'css-loader' } 55 | ] 56 | } 57 | ] 58 | } 59 | } 60 | 61 | module.exports = config 62 | -------------------------------------------------------------------------------- /lib/client/App.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | default as React // eslint-disable-line 4 | } from 'react' 5 | import { 6 | Tabs, 7 | Tab 8 | } from 'react-bootstrap' // ~190kb 9 | import TreeByPlugin from './Components/TreeByPlugin' 10 | import Log from './Components/Log' 11 | import request from 'browser-request' 12 | import update from 'immutability-helper' 13 | 14 | export default class App extends Component { 15 | constructor () { 16 | super() 17 | const app = this 18 | this.state = { 19 | plugins: [], 20 | log: [] 21 | } 22 | request('/debug-ui/data.json', (err, response, body) => { 23 | if (err) throw new Error(err) 24 | body = JSON.parse(body) 25 | app.setState(body) 26 | }) 27 | } 28 | updateState (object, spec, callback = () => {}) { 29 | this.setState(update(object, spec), callback) 30 | } 31 | render () { 32 | return ( 33 |
34 |
35 |
36 |

Metalsmith Debug UI

37 |
38 |
39 |
40 |
41 | 42 | 43 | 47 | 48 | 49 | 53 | 54 | 55 | 58 | 59 | 60 |
61 |
62 |
63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Metalsmith Debug UI 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 |
23 |
24 | 25 |
26 | 27 | 28 |
29 |
30 |
31 | 35 | 36 |
37 |
38 |

No data yet :(

39 |
40 |
41 |

No data yet :(

42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /lib/client/Components/PluginList.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | default as React // eslint-disable-line 4 | } from 'react' 5 | // import update from 'immutability-helper' 6 | // import 'isomorphic-fetch' 7 | import PropTypes from 'prop-types' 8 | import Plugin from './Plugin' 9 | // import { 10 | // Button, 11 | // Badge 12 | // } from 'react-bootstrap' 13 | 14 | export default class PluginList extends Component { 15 | // constructor (props) { 16 | // super(props) 17 | // } 18 | // updateState (...args) { 19 | // const callback = args.pop() 20 | // this.setstate(update(...args), callback) 21 | // } 22 | // onClick () { 23 | // this.props.setKeywordState(this.props.name, !this.state.isActive) 24 | // this.updateState(this.state, {isActive: {$set: !this.state.isActive}}) 25 | // } 26 | // setActivePlugin (idx) { 27 | // this.props.setActivePlugin(idx) 28 | // this.updateState(this.state, {activeIdx: {$set: idx}}) 29 | // } 30 | render () { 31 | const plugins = this.props.plugins.map((plugin, idx) => { 32 | return ( 33 | 40 | ) 41 | }) || '' 42 | return ( 43 |
44 | {plugins} 45 |
46 | ) 47 | // if (this.props.appearances < 2 && !this.props.showSingles) return null 48 | // return ( 49 | // 56 | // ) 57 | } 58 | } 59 | PluginList.propTypes = { 60 | plugins: PropTypes.array.isRequired, 61 | activeIdx: PropTypes.number.isRequired, 62 | setActivePlugin: PropTypes.func.isRequired 63 | } 64 | -------------------------------------------------------------------------------- /lib/client/Components/TreeByPlugin.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | default as React // eslint-disable-line 4 | } from 'react' 5 | import update from 'immutability-helper' 6 | // import 'isomorphic-fetch' 7 | import PropTypes from 'prop-types' 8 | import JSONTree from 'react-json-tree' 9 | import PluginList from './PluginList' 10 | // import { 11 | // Button, 12 | // Badge 13 | // } from 'react-bootstrap' 14 | 15 | export default class TreeByPlugin extends Component { 16 | constructor (props) { 17 | super(props) 18 | this.state = { activeIdx: 0 } 19 | // this.onClick = this.onClick.bind(this) 20 | this.setActivePlugin = this.setActivePlugin.bind(this) 21 | } 22 | updateState (object, spec, callback = () => {}) { 23 | this.setState(update(object, spec), callback) 24 | } 25 | // onClick () { 26 | // this.props.setKeywordState(this.props.name, !this.state.isActive) 27 | // this.updateState(this.state, {isActive: {$set: !this.state.isActive}}) 28 | // } 29 | setActivePlugin (idx) { 30 | // this.props.setActivePlugin(idx) 31 | this.updateState(this.state, {activeIdx: {$set: idx}}) 32 | } 33 | render () { 34 | let pluginList 35 | let jsonTree 36 | if (this.props.plugins.length) { 37 | pluginList = ( 38 | 43 | ) 44 | jsonTree = ( 45 | 50 | ) 51 | } else { 52 | pluginList = ( 53 |

Waiting for plugins

54 | ) 55 | jsonTree = ( 56 |

Waiting for data

57 | ) 58 | } 59 | return ( 60 |
61 |
62 |
63 | {pluginList} 64 |
65 |
66 | {jsonTree} 67 |
68 |
69 |
70 | ) 71 | } 72 | } 73 | 74 | const theme = { 75 | scheme: 'monokai', 76 | author: 'wimer hazenberg (http://www.monokai.nl)', 77 | base00: '#272822', 78 | base01: '#383830', 79 | base02: '#49483e', 80 | base03: '#75715e', 81 | base04: '#a59f85', 82 | base05: '#f8f8f2', 83 | base06: '#f5f4f1', 84 | base07: '#f9f8f5', 85 | base08: '#f92672', 86 | base09: '#fd971f', 87 | base0A: '#f4bf75', 88 | base0B: '#a6e22e', 89 | base0C: '#a1efe4', 90 | base0D: '#66d9ef', 91 | base0E: '#ae81ff', 92 | base0F: '#cc6633' 93 | } 94 | TreeByPlugin.propTypes = { 95 | plugins: PropTypes.array.isRequired, 96 | dataProperty: PropTypes.string.isRequired 97 | } 98 | -------------------------------------------------------------------------------- /.README.md: -------------------------------------------------------------------------------- 1 | # ${pkg.name} 2 | 3 | ${badge('nodei')} 4 | 5 | ${badge('npm')} ${badge('github-issues')} ${badge('github-stars')} ${badge('github-forks')} 6 | 7 | Browser based debug interface for [metalsmith](https://metalsmith.io) 8 | 9 | Provides nice ui to navigate metalsmith files and metadata, allowing you to view any stage of the build process 10 | 11 | Features: 12 | 13 | * nice ui for exploring files & metadata 14 | * can jump forwards and backwards through the build process 15 | * cool react based client 16 | 17 | ![files interface][files] 18 | 19 | See the [annotated source][annotated source] or [github repo][github repo] 20 | 21 | ## install 22 | 23 | `npm i --save ${pkg.name}` 24 | 25 | ## usage 26 | `metalsmith-debug-ui` clones your metalsmith files and metadata strutures at 27 | different times during the build process and stores this history. Then it 28 | injects a browser based client into your build output which allows you to view 29 | that history. 30 | 31 | ### patch mode 32 | This will report after every plugin. You need to patch your metalsmith instance. 33 | 34 | ```javascript 35 | import Metalsmith from 'metalsmith' 36 | import { patch } from 'metalsmith-debug-ui' 37 | 38 | let ms = Metalsmith('src') 39 | 40 | patch(ms) 41 | 42 | ms 43 | .use(...) 44 | .build(...) 45 | ``` 46 | 47 | ### report mode 48 | 49 | Just call `report` as a plugin 50 | 51 | ```javascript 52 | import Metalsmith from 'metalsmith' 53 | import { report } from 'metalsmith-debug-ui' 54 | 55 | let ms = Metalsmith('src') // no need to patch 56 | 57 | ms 58 | .use(myFirstPlugin({...})) 59 | .use(mySecondPlugin({...})) 60 | .use(report('stage 1')) 61 | .use(myFirstPlugin({...})) 62 | .use(report('stage 2')) 63 | .build(...) 64 | ``` 65 | 66 | ### metalsmith CLI / metalsmith.json 67 | This plugin won't work in metalsmith CLI mode. 68 | 69 | ### viewing output 70 | The client should be built with the rest of your site, and will be located at `debug-ui/index.html` in your build directory. You should use your favourite static development server to view it in the same way you would view anything else in your build directory. 71 | 72 | ### errors during build 73 | When a plugin throws an error metalsmith will just die as per normal behaviour, but the data debug-ui has collected will still be written to the build dir. 74 | 75 | The only problem is that if you're using a dev server like `metalsmith-dev-server` you won't be able to view the ui to see what went wrong. I recommend implementing [browser-sync][browser-sync] or something instead. 76 | 77 | ### anonymous plugins 78 | It's difficult to reliably detect the names of plugins in order to report them in the ui. debug-ui first tries to sniff the plugin name from a stack trace, if that fails it checks whether the plugin returns a named function, and if that fails it will simply list the plugin as `anonymous`. 79 | 80 | In most cases this is satisfactory. If something is reported as anonymous you can easily work out what it is by looking at the plugins before and after it. 81 | 82 | ## demo 83 | see [metalsmith-all-the-things][metalsmith-all-the-things] for a full working 84 | demo. 85 | 86 | ## options 87 | nil 88 | 89 | ## plugin compatibility 90 | Some plugins may not write to the `debug-ui` log, although I haven't found any 91 | yet. In theory any plugins using `debug v2.x.x` should work. If you find one 92 | please post an issue. 93 | 94 | ## testing 95 | nil. 96 | 97 | ## building 98 | Deprecation warning re: parseQuery is from upstream package. Don't worry about 99 | it. 100 | 101 | `npm run watch` 102 | 103 | ## Author 104 | Levi Wheatcroft 105 | 106 | ## Contributing 107 | Contributions welcome; Please submit all pull requests against the master 108 | branch. 109 | 110 | ## License 111 | **MIT** : http://opensource.org/licenses/MIT 112 | 113 | [annotated source]: https://leviwheatcroft.github.io/metalsmith-debug-ui "annotated source" 114 | [github repo]: https://github.com/leviwheatcroft/metalsmith-debug-ui "github repo" 115 | [files]: http://leviwheatcroft.github.io/metalsmith-debug-ui/images/files.png 116 | [browser-sync]: https://www.browsersync.io/ 117 | [metalsmith-all-the-things]: https://github.com/leviwheatcroft/metalsmith-all-the-things 118 | [anonymous vs named plugins]: https://github.com/leviwheatcroft/metalsmith-debug-ui/issues/2 119 | [metalsmith-sugar]: https://github.com/connected-world-services/metalsmith-sugar 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # metalsmith-debug-ui 2 | 3 | ![nodei.co](https://nodei.co/npm/metalsmith-debug-ui.png?downloads=true&downloadRank=true&stars=true) 4 | 5 | ![npm](https://img.shields.io/npm/v/metalsmith-debug-ui.svg) ![github-issues](https://img.shields.io/github/issues/leviwheatcroft/metalsmith-debug-ui.svg) ![stars](https://img.shields.io/github/stars/leviwheatcroft/metalsmith-debug-ui.svg) ![forks](https://img.shields.io/github/forks/leviwheatcroft/metalsmith-debug-ui.svg) 6 | 7 | Browser based debug interface for [metalsmith](https://metalsmith.io) 8 | 9 | Provides nice ui to navigate metalsmith files and metadata, allowing you to view any stage of the build process 10 | 11 | Features: 12 | 13 | * nice ui for exploring files & metadata 14 | * can jump forwards and backwards through the build process 15 | * cool react based client 16 | 17 | ![files interface][files] 18 | 19 | See the [annotated source][annotated source] or [github repo][github repo] 20 | 21 | ## install 22 | 23 | `npm i --save metalsmith-debug-ui` 24 | 25 | ## usage 26 | `metalsmith-debug-ui` clones your metalsmith files and metadata strutures at 27 | different times during the build process and stores this history. Then it 28 | injects a browser based client into your build output which allows you to view 29 | that history. 30 | 31 | ### patch mode 32 | This will report after every plugin. You need to patch your metalsmith instance. 33 | 34 | ```javascript 35 | import Metalsmith from 'metalsmith' 36 | import { patch } from 'metalsmith-debug-ui' 37 | 38 | let ms = Metalsmith('src') 39 | 40 | patch(ms) 41 | 42 | ms 43 | .use(...) 44 | .build(...) 45 | ``` 46 | 47 | ### report mode 48 | 49 | Just call `report` as a plugin 50 | 51 | ```javascript 52 | import Metalsmith from 'metalsmith' 53 | import { report } from 'metalsmith-debug-ui' 54 | 55 | let ms = Metalsmith('src') // no need to patch 56 | 57 | ms 58 | .use(myFirstPlugin({...})) 59 | .use(mySecondPlugin({...})) 60 | .use(report('stage 1')) 61 | .use(myFirstPlugin({...})) 62 | .use(report('stage 2')) 63 | .build(...) 64 | ``` 65 | 66 | ### metalsmith CLI / metalsmith.json 67 | This plugin won't work in metalsmith CLI mode. 68 | 69 | ### viewing output 70 | The client should be built with the rest of your site, and will be located at `debug-ui/index.html` in your build directory. You should use your favourite static development server to view it in the same way you would view anything else in your build directory. 71 | 72 | ### errors during build 73 | When a plugin throws an error metalsmith will just die as per normal behaviour, but the data debug-ui has collected will still be written to the build dir. 74 | 75 | The only problem is that if you're using a dev server like `metalsmith-dev-server` you won't be able to view the ui to see what went wrong. I recommend implementing [browser-sync][browser-sync] or something instead. 76 | 77 | ### anonymous plugins 78 | It's difficult to reliably detect the names of plugins in order to report them in the ui. debug-ui first tries to sniff the plugin name from a stack trace, if that fails it checks whether the plugin returns a named function, and if that fails it will simply list the plugin as `anonymous`. 79 | 80 | In most cases this is satisfactory. If something is reported as anonymous you can easily work out what it is by looking at the plugins before and after it. 81 | 82 | ## demo 83 | see [metalsmith-all-the-things][metalsmith-all-the-things] for a full working 84 | demo. 85 | 86 | ## options 87 | nil 88 | 89 | ## plugin compatibility 90 | Some plugins may not write to the `debug-ui` log, although I haven't found any 91 | yet. In theory any plugins using `debug v2.x.x` should work. If you find one 92 | please post an issue. 93 | 94 | ## testing 95 | nil. 96 | 97 | ## building 98 | Deprecation warning re: parseQuery is from upstream package. Don't worry about 99 | it. 100 | 101 | `npm run watch` 102 | 103 | ## Author 104 | Levi Wheatcroft 105 | 106 | ## Contributing 107 | Contributions welcome; Please submit all pull requests against the master 108 | branch. 109 | 110 | ## License 111 | **MIT** : http://opensource.org/licenses/MIT 112 | 113 | [annotated source]: https://leviwheatcroft.github.io/metalsmith-debug-ui "annotated source" 114 | [github repo]: https://github.com/leviwheatcroft/metalsmith-debug-ui "github repo" 115 | [files]: http://leviwheatcroft.github.io/metalsmith-debug-ui/images/files.png 116 | [browser-sync]: https://www.browsersync.io/ 117 | [metalsmith-all-the-things]: https://github.com/leviwheatcroft/metalsmith-all-the-things 118 | [anonymous vs named plugins]: https://github.com/leviwheatcroft/metalsmith-debug-ui/issues/2 119 | [metalsmith-sugar]: https://github.com/connected-world-services/metalsmith-sugar 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metalsmith-debug-ui", 3 | "author": "Levi Wheatcroft (http://leviwheatcroft.com)", 4 | "description": "A metalsmith plugin providing ui to navigate files and meta during build", 5 | "repository": "git://github.com/leviwheatcroft/metalsmith-debug-ui.git", 6 | "homepage": "http://leviwheatcroft.github.io/metalsmith-debug-ui", 7 | "version": "0.3.3", 8 | "keywords": [ 9 | "metalsmith", 10 | "debug" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/leviwheatcroft/metalsmith-debug-ui/issues" 14 | }, 15 | "license": "MIT", 16 | "main": "dist/index.js", 17 | "scripts": { 18 | "build": "npm run babel && npm run client:prodn && npm run docs && cp docs/README.md.html docs/index.html && npm run gh-pages", 19 | "client": "cross-env DEBUG=metalsmith* babel-node build", 20 | "client:prodn": "cross-env DEBUG=metalsmith* NODE_ENV=production babel-node build", 21 | "readme": "node-readme", 22 | "babel": "cross-env NODE_ENV=node6 babel lib -d dist --ignore client", 23 | "babel:watch": "cross-env NODE_ENV=node6 babel lib --watch -d dist --ignore client", 24 | "docs": "npm run readme && rm -fr ./docs/* && docker -o ./docs -I -x dist,.README.md,test/fixtures,node_modules,docs,.store,config && cp --parents images/* docs", 25 | "gh-pages": "gh-pages -d docs", 26 | "test:coverage": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text --check-coverage --lines 100 npm run test", 27 | "test": "cross-env NODE_ENV=test mocha --compilers js:babel-register test", 28 | "test:watch": "cross-env NODE_ENV=test mocha --compilers js:babel-register --watch test", 29 | "version": "npm run build", 30 | "postversion": "git push && git push --tags", 31 | "watch": "npm-watch" 32 | }, 33 | "watch": { 34 | "babel": { 35 | "patterns": [ 36 | "lib" 37 | ], 38 | "ignore": [ 39 | "lib/client", 40 | "lib/build.js" 41 | ] 42 | }, 43 | "client": { 44 | "patterns": [ 45 | "lib/client", 46 | "dist/build.js" 47 | ], 48 | "extensions": "js,jsx,html,less" 49 | } 50 | }, 51 | "dependencies": { 52 | "debug": "^2.6.8", 53 | "lodash.set": "^4.3.2", 54 | "lodash.transform": "^4.6.0", 55 | "strip-ansi": "^3.0.1", 56 | "vow": "^0.4.14", 57 | "wrap-fn": "^0.1.5" 58 | }, 59 | "devDependencies": { 60 | "babel-cli": "^6.16.0", 61 | "babel-core": "^6.26.3", 62 | "babel-eslint": "^7.0.0", 63 | "babel-loader": "^7.1.2", 64 | "babel-plugin-add-module-exports": "^0.2.1", 65 | "babel-plugin-istanbul": "^2.0.1", 66 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 67 | "babel-preset-es2015": "^6.24.1", 68 | "babel-preset-react": "^6.24.1", 69 | "babel-preset-stage-0": "^6.16.0", 70 | "babel-register": "^6.16.3", 71 | "bootstrap": "^3.3.7", 72 | "bootstrap-webpack": "0.0.6", 73 | "browser-request": "^0.3.3", 74 | "chai": "^3.5.0", 75 | "concurrently": "^3.4.0", 76 | "config": "^1.24.0", 77 | "cross-env": "^3.1.3", 78 | "css-loader": "^0.28.0", 79 | "docker": "^1.0.0", 80 | "eslint": "^3.7.1", 81 | "eslint-config-standard": "^6.2.0", 82 | "eslint-plugin-babel": "^3.3.0", 83 | "eslint-plugin-promise": "^3.0.0", 84 | "eslint-plugin-react": "^6.10.3", 85 | "eslint-plugin-standard": "^2.0.1", 86 | "exports-loader": "^0.6.4", 87 | "extract-text-webpack-plugin": "^2.1.0", 88 | "file-loader": "^0.11.1", 89 | "gh-pages": "^0.12.0", 90 | "hjson": "^2.4.1", 91 | "immutability-helper": "^2.2.2", 92 | "imports-loader": "^0.7.1", 93 | "jquery": "^3.2.1", 94 | "jsonview": "^1.2.0", 95 | "jstransformer": "^1.0.0", 96 | "jstransformer-pug": "^0.2.3", 97 | "less-loader": "^4.0.3", 98 | "metalsmith": "^2.3.0", 99 | "metalsmith-ignore": "^1.0.0", 100 | "metalsmith-in-place": "^2.0.1", 101 | "metalsmith-less": "^2.0.0", 102 | "metalsmith-webpack-2": "^1.0.5", 103 | "mocha": "^5.2.0", 104 | "mocha-eslint": "^3.0.1", 105 | "nock": "^9.0.2", 106 | "node-readme": "^0.1.9", 107 | "node-resemble-js": "^0.1.1", 108 | "npm-watch": "^0.1.8", 109 | "nyc": "^13.1.0", 110 | "react": "^15.5.4", 111 | "react-bootstrap": "^0.30.8", 112 | "react-cookie": "^1.0.5", 113 | "react-dom": "^15.5.4", 114 | "react-filterable-table": "^0.3.0", 115 | "react-json-tree": "^0.10.7", 116 | "sinon": "^1.17.7", 117 | "socket.io-client": "^2.1.1", 118 | "style-loader": "^0.16.1", 119 | "url-loader": "^1.1.2", 120 | "webpack": "^2.3.3" 121 | }, 122 | "eslintConfig": { 123 | "rules": { 124 | "react/jsx-uses-vars": [ 125 | 2 126 | ] 127 | }, 128 | "parser": "babel-eslint", 129 | "extends": "standard", 130 | "installedESLint": true, 131 | "plugins": [ 132 | "standard", 133 | "babel", 134 | "react" 135 | ], 136 | "env": { 137 | "node": true, 138 | "mocha": true 139 | } 140 | }, 141 | "babel": { 142 | "presets": [ 143 | "stage-0", 144 | "es2015" 145 | ] 146 | }, 147 | "nyc": { 148 | "include": [ 149 | "lib/**/*.js" 150 | ], 151 | "require": [ 152 | "babel-register" 153 | ], 154 | "sourceMap": false, 155 | "instrument": false 156 | }, 157 | "directories": { 158 | "test": "test" 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import vow from 'vow' 2 | import wrap from 'wrap-fn' 3 | import debug from 'debug' 4 | import { 5 | sep, 6 | parse, 7 | join 8 | } from 'path' 9 | import { 10 | readdir, 11 | readFile 12 | } from 'fs' 13 | import transform from 'lodash.transform' 14 | import set from 'lodash.set' 15 | import stripAnsi from 'strip-ansi' 16 | import util from 'util' 17 | 18 | // used for debug messages 19 | const dbg = debug('metalsmith-debug-ui') 20 | 21 | // stores cloned data & log until written to client 22 | const data = { 23 | log: [], 24 | plugins: [] 25 | } 26 | 27 | // whether client html, styles & js has already been written to build dir 28 | let clientWritten = false 29 | 30 | /** 31 | * ## log 32 | * this log fn is stashed on metalsmith instance to allow other plugins to 33 | * write to this debug ui. 34 | * @param ...args accepts parameters in the same way `debug.log` does 35 | */ 36 | function log (...args) { 37 | let clean = util.format.apply(util, args.map(stripAnsi)) 38 | let entry = /^\s*([^\s]*)\s(.*)\s([^\s]*)$/.exec(clean) 39 | data.log.push({ 40 | timestamp: new Date().toISOString().slice(11, -1), 41 | plugin: entry ? entry[1] : '', 42 | message: entry ? entry[2] : clean, // fallback failed regex 43 | elapsed: entry ? entry[3] : '' 44 | }) 45 | // write to console as normal, breaks a lot of debug configuration but w/e 46 | process.stdout.write(util.format.apply(util, args) + '\n') 47 | } 48 | 49 | // hook `debug.log` 50 | debug.log = log 51 | 52 | /** 53 | * ## patch 54 | * patches `metalsmith.build`, when `build` is called it will wrap all plugins 55 | * with reporter. 56 | * ``` 57 | * let metalsmith = new Metalsmith(__dirname) 58 | * patch(metalsmith) 59 | * ``` 60 | * @param {object} metalsmith instance 61 | */ 62 | function patch (metalsmith, options) { 63 | dbg('patched build fn') 64 | metalsmith._maskedBuild = metalsmith.build 65 | metalsmith.build = build 66 | metalsmith.log = log 67 | return metalsmith 68 | } 69 | 70 | /** 71 | * ## report 72 | * to be called as a metalsmith plugin clones current state of files & metadata 73 | * @param {String} name 74 | * @returns {Promise} 75 | */ 76 | function report (fnName) { 77 | return function (files, metalsmith) { 78 | pushData(fnName, files, metalsmith) 79 | writeData(files, metalsmith) 80 | return writeClient(files, metalsmith) // promise 81 | } 82 | } 83 | 84 | /** 85 | * ## clear 86 | * clears data structure 87 | * useful for plugins that manage the build process like 88 | * `metalsmith-browser-sync` see issue #4 89 | */ 90 | function clear () { 91 | data.log = [] 92 | data.plugins = [] 93 | } 94 | 95 | /** 96 | * ## build 97 | * masks metalsmith's build fn. Once patched, calling build will wrap all 98 | * plugins with the debug-ui recorder, before calling the original build fn. 99 | * 100 | * @param ...args same args as metalsmith build 101 | */ 102 | function build (callback) { 103 | // `masked` will become the new plugin array 104 | const masked = [] 105 | // before running any plugins, write the client to the build dir 106 | masked.push(writeClient) 107 | // wrap all existing plugins to capture data 108 | this.plugins.forEach((fn) => { 109 | masked.push((files, metalsmith) => { 110 | let fnName 111 | return vow.resolve() 112 | .then(() => { 113 | let defer = vow.defer() 114 | // wrap here to support sync, async, and promises.. like metalsmith 115 | // this also traps exceptions 116 | let wrapped = wrap(fn, (err) => { 117 | // we need to try to sniff the plugin name here, because it's likely 118 | // in the stack 119 | // console.log(new Error().stack) 120 | 121 | // regex to match a string like `/metalsmith-layouts/` 122 | let regex = /\/metalsmith-((?!debug-ui)[\w-]*)[/.](?!.*metalsmith-)/m 123 | // apply to a stack trace (non-standard) 124 | let match = regex.exec(new Error().stack) 125 | if (match) fnName = match[1] 126 | // if that didn't work, try for a named function 127 | else if (fn.name) fnName = fn.name.replace(/bound /, '') 128 | // fall back to anonymous :( 129 | else fnName = 'anonymous' 130 | if (err) defer.reject(err) 131 | else defer.resolve() 132 | }) 133 | wrapped(files, metalsmith) 134 | return defer.promise() 135 | }) 136 | .then(() => pushData(fnName, files, metalsmith)) 137 | .catch((err) => { 138 | return writeData(files, metalsmith) 139 | .then(() => { throw err }) 140 | }) 141 | }) 142 | }) 143 | // after all plugins have run, write data to build dir 144 | masked.push(writeData) 145 | 146 | this.plugins = masked 147 | // run metalsmith's original build 148 | this._maskedBuild(callback) 149 | } 150 | 151 | /** 152 | * ## pushData 153 | * clones current files & meta into store 154 | * @param {Object} files metalsmith files structure 155 | * @param {Metalsmith} metalsmith instance 156 | * @param {String} name descriptor for ui, usually plugin fn name 157 | */ 158 | function pushData (fnName, files, metalsmith) { 159 | data.plugins.push({ 160 | // use fn.name to give user some idea where we're up to 161 | // name of bound function is 'bound functionName' 162 | fnName, 163 | // convert files structure to directory tree 164 | files: tree(render(files)), 165 | // normal metalsmith metadata 166 | metadata: render(metalsmith.metadata()) 167 | }) 168 | } 169 | 170 | /** 171 | * ## injectData 172 | * writes `data.json` to metalsmith files structure 173 | * @param {Object} files metalsmith files structure 174 | */ 175 | function writeData (files, metalsmith) { 176 | dbg('writing data to build') 177 | let defer = vow.defer() 178 | let dataJson = { 179 | 'debug-ui/data.json': { 180 | contents: Buffer.from(JSON.stringify(data)) 181 | } 182 | } 183 | // write the history data, no need for async 184 | metalsmith.write(dataJson, defer.resolve.bind(defer)) 185 | return defer.promise() 186 | } 187 | 188 | /** 189 | * ## writeClient 190 | * writes html, styles, and js to build dir 191 | * @param {Object} files metalsmith files structure 192 | * @param {Metalsmith} metalsmith 193 | * @returns {Promise} 194 | */ 195 | function writeClient (files, metalsmith) { 196 | if (clientWritten) return vow.resolve() 197 | clientWritten = true 198 | const defer = vow.defer() 199 | // scrape the client dir and inject into files 200 | readdir(join(__dirname, 'client'), (err, children) => { 201 | if (err) throw new Error(err) 202 | const workers = [] 203 | const client = [] 204 | children.forEach((child) => { 205 | dbg(join(__dirname, 'client', child)) 206 | 207 | let worker = readFilePromise(join(__dirname, 'client', child)) 208 | .then((contents) => { 209 | dbg(join('debug-ui', child)) 210 | client[join('debug-ui', child)] = { contents } 211 | }) 212 | workers.push(worker) 213 | }) 214 | vow.all(workers) 215 | .then(() => { 216 | metalsmith.write(client, defer.resolve.bind(defer)) 217 | }) 218 | .catch(defer.reject.bind(defer)) 219 | }) 220 | return defer.promise() 221 | } 222 | 223 | /** 224 | * ## readFilePromise 225 | * promisify readFile 226 | * @param {String} path 227 | * @returns {Promise} 228 | */ 229 | function readFilePromise (path) { 230 | const defer = vow.defer() 231 | readFile(path, (err, contents) => { 232 | if (err) return defer.reject(err) 233 | defer.resolve(contents) 234 | }) 235 | return defer.promise() 236 | } 237 | 238 | /** 239 | * ## tree fn 240 | * convert files structure to directory tree 241 | * 242 | * @param {object} files metalsmith files structure 243 | */ 244 | function tree (files) { 245 | return transform( 246 | files, 247 | function (result, val, key) { 248 | let path = parse(key) 249 | if (path.dir) { 250 | set(result, path.dir.split(sep).concat(path.base), val) 251 | } else { 252 | set(result, path.base, val) 253 | } 254 | }, 255 | {} 256 | ) 257 | } 258 | 259 | /** 260 | * ## render 261 | * Really rad fn to parse an object, convert values to something renderable, 262 | * and avoid multiple copies of the same object (recursion, cyclic references, 263 | * or just copies) 264 | * 265 | * @param {object} obj target, files or metadata 266 | */ 267 | function render (obj) { 268 | // use copy so we don't mutate files 269 | let copy = {} 270 | // store seen objects so we can avoid re-printing them 271 | let list = [ obj ] 272 | // store paths so we can assign converted values to the right path 273 | let paths = [['root']] 274 | for (let idx = 0; idx < list.length; idx++) { 275 | let item = list[idx] 276 | Object.keys(item).forEach((key) => { 277 | // store path of current item 278 | let path = paths[idx].concat([key]) 279 | if (key === 'contents') { 280 | return set(copy, path, '...') 281 | } 282 | if (Buffer.isBuffer(item[key])) { 283 | return set(copy, path, item[key].toString()) 284 | } 285 | // check if this item has been rendered already 286 | let copyIdx = list.indexOf(item[key]) 287 | if (~copyIdx) { 288 | return set(copy, path, `[Copy: ${paths[copyIdx].join(' > ')}]`) 289 | } 290 | // store objects so we can assess them next loop 291 | if (item[key] instanceof Object) { 292 | list.push(item[key]) 293 | paths.push(path) 294 | return 295 | } 296 | // if none ofthe above apply, just stash the value 297 | set(copy, path, item[key]) 298 | }) 299 | } 300 | return copy.root 301 | } 302 | 303 | /** 304 | * ## exports 305 | */ 306 | export default { patch, report, clear } 307 | export { patch, report, clear } 308 | --------------------------------------------------------------------------------