diff --git a/packages/docz-core/src/commands/args.ts b/packages/docz-core/src/commands/args.ts index 4d1cb5692..f3fca15c8 100644 --- a/packages/docz-core/src/commands/args.ts +++ b/packages/docz-core/src/commands/args.ts @@ -5,10 +5,6 @@ export interface Argv { /* io args */ src: string files: string - /* template args */ - title: string - description: string - theme: string /* bundler args */ env: string debug: boolean @@ -17,6 +13,10 @@ export interface Argv { port: number websocketPort: number websocketHost: string + /* template args */ + title: string + description: string + theme: string } export interface Config extends Argv { @@ -24,6 +24,9 @@ export interface Config extends Argv { plugins?: Plugin[] mdPlugins: any[] hastPlugins: any[] + themeConfig?: { + [key: string]: any + } } export const args = (yargs: any) => { diff --git a/packages/docz-core/src/commands/dev.ts b/packages/docz-core/src/commands/dev.ts index ed1ab6cdd..ed07d998b 100644 --- a/packages/docz-core/src/commands/dev.ts +++ b/packages/docz-core/src/commands/dev.ts @@ -1,6 +1,6 @@ -import { load } from 'load-cfg' +import { load, finds } from 'load-cfg' import chokidar from 'chokidar' -import WebSocket from 'ws' +import WS from 'ws' import * as paths from '../config/paths' import { Config } from './args' @@ -11,25 +11,29 @@ import { webpack } from '../bundlers' process.env.BABEL_ENV = process.env.BABEL_ENV || 'development' process.env.NODE_ENV = process.env.NODE_ENV || 'development' +const isSocketOpened = (socket: WS) => socket.readyState === WS.OPEN + const entriesData = (entries: EntryMap, config: Config) => - JSON.stringify({ type: 'entries data', data: entries }) + JSON.stringify({ type: 'docz.entries', data: entries }) -const updateEntries = (socket: WebSocket) => (config: Config) => async () => { - const newEntries = new Entries(config) - const newMap = await newEntries.getMap() +const updateEntries = (socket: WS) => (config: Config) => async () => { + if (isSocketOpened(socket)) { + const newEntries = new Entries(config) + const newMap = await newEntries.getMap() - await Entries.rewrite(newMap) - socket.send(entriesData(newMap, config)) + await Entries.rewrite(newMap) + socket.send(entriesData(newMap, config)) + } } -const processEntries = (config: Config) => async (ws: WebSocket.Server) => { +const processEntries = (config: Config) => async (ws: WS.Server) => { const entries = new Entries(config) const map = await entries.getMap() const watcher = chokidar.watch(config.files, { ignored: /(^|[\/\\])\../, }) - const handleConnection = async (socket: WebSocket) => { + const handleConnection = async (socket: WS) => { const update = updateEntries(socket) socket.send(entriesData(map, config)) @@ -44,24 +48,54 @@ const processEntries = (config: Config) => async (ws: WebSocket.Server) => { ws.on('connection', handleConnection) ws.on('close', () => watcher.close()) + await Entries.write(config, map) } -const INITIAL_CONFIG = { - paths, - plugins: [], - mdPlugins: [], - hastPlugins: [], +const configData = (config: Config) => + JSON.stringify({ type: 'docz.config', data: config.themeConfig }) + +const updateConfig = (socket: WS, args: Config) => () => { + const config = load('docz', {}, true) + + if (isSocketOpened(socket)) { + socket.send(configData(config)) + } +} + +const processThemeConfig = (config: Config) => async (ws: WS.Server) => { + const watcher = chokidar.watch(finds('docz')) + + const handleConnection = async (socket: WS) => { + const update = updateConfig(socket, config) + + watcher.on('add', update) + watcher.on('change', update) + watcher.on('unlink', update) + + update() + } + + ws.on('connection', handleConnection) + ws.on('close', () => watcher.close()) } export const dev = async (args: Config) => { - const config = load('docz', { ...args, ...INITIAL_CONFIG }) + const config = load('docz', { + ...args, + paths, + plugins: [], + mdPlugins: [], + hastPlugins: [], + themeConfig: {}, + }) + const bundler = webpack(config) const server = await bundler.createServer(bundler.getConfig()) const app = await server.start() app.on('listening', async server => { - const ws = new WebSocket.Server({ + const ws = new WS.Server({ server, host: config.websocketHost, port: config.websocketPort, @@ -75,6 +109,7 @@ export const dev = async (args: Config) => { process.on('exit', handleClose) process.on('SIGINT', handleClose) - await processEntries(args)(ws) + await processEntries(config)(ws) + await processThemeConfig(config)(ws) }) } diff --git a/packages/docz-core/templates/app.tpl.js b/packages/docz-core/templates/app.tpl.js index ae8b7fcd3..ce532c541 100644 --- a/packages/docz-core/templates/app.tpl.js +++ b/packages/docz-core/templates/app.tpl.js @@ -16,12 +16,14 @@ const Wrapper = props => class App extends React.Component { state = { + config: {}, entries: {}, - imports: {} + imports: {}, } static getDerivedStateFromProps(nextProps, prevState) { return { + config: prevState.config, entries: prevState.entries, imports: nextProps.imports } @@ -31,9 +33,13 @@ class App extends React.Component { socket.onmessage = ev => { const message = JSON.parse(ev.data) - if (message.type === 'entries data') { + if (message.type === 'docz.entries') { this.setState({ entries: message.data }) } + + if (message.type === 'docz.config') { + this.setState({ config: message.data }) + } } } diff --git a/packages/docz/package.json b/packages/docz/package.json index 0af50f81b..e29f93951 100644 --- a/packages/docz/package.json +++ b/packages/docz/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@sindresorhus/slugify": "^0.3.0", + "deepmerge": "^2.1.0", "invariant": "^2.2.4", "loadable-components": "^2.1.0", "pascalcase": "^0.1.1", @@ -37,6 +38,7 @@ }, "devDependencies": { "@types/bluebird": "^3.5.20", + "@types/deepmerge": "^2.1.0", "@types/react": "^16.3.12", "@types/react-dom": "^16.0.5", "@types/react-router-dom": "^4.2.6" diff --git a/packages/docz/src/theme.tsx b/packages/docz/src/theme.tsx index 13c7bdba8..e59ef4e9f 100644 --- a/packages/docz/src/theme.tsx +++ b/packages/docz/src/theme.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { ComponentType as CT } from 'react' import { BrowserRouter } from 'react-router-dom' +import merge from 'deepmerge' export type MSXComponent = CT<{ components: { [key: string]: any } @@ -20,17 +21,23 @@ export interface Entry { order: number } +export interface ThemeConfig { + [key: string]: any +} + export type EntryMap = Record export type ImportMap = Record Promise> export interface DataContext { - imports: ImportMap + config: ThemeConfig entries: EntryMap + imports: ImportMap } const initialContext: DataContext = { - imports: {}, + config: {}, entries: {}, + imports: {}, } export const dataContext = React.createContext(initialContext) @@ -40,12 +47,20 @@ export interface ThemeProps extends DataContext { children(WrappedComponent: CT): JSX.Element } -export function theme(WrappedComponent: CT): CT { +export function theme( + WrappedComponent: CT, + defaultConfig?: ThemeConfig +): CT { const Theme: CT = props => { - const { wrapper: Wrapper } = props + const { wrapper: Wrapper, entries, imports, config = {} } = props + const value = { + entries, + imports, + config: merge(defaultConfig || {}, config), + } return ( - +