├── 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 |
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 | 
4 |
5 |    
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 |
--------------------------------------------------------------------------------