From 2f073d5477e0bfcb5e0e92cb61523d078a86e60f Mon Sep 17 00:00:00 2001 From: Pedro Nauck Date: Mon, 16 Apr 2018 13:07:18 -0300 Subject: [PATCH] feat(docz-core): add chokidar to watch new entries --- packages/docz-core/src/Bundler.ts | 91 ++-------------------------- packages/docz-core/src/Entries.ts | 98 +++++++++++++++++++++++++++---- packages/docz-core/src/Entry.ts | 12 ++-- packages/docz-core/src/Server.ts | 82 ++++++++++++++++++-------- 4 files changed, 153 insertions(+), 130 deletions(-) diff --git a/packages/docz-core/src/Bundler.ts b/packages/docz-core/src/Bundler.ts index 5644576..ffe0b16 100644 --- a/packages/docz-core/src/Bundler.ts +++ b/packages/docz-core/src/Bundler.ts @@ -1,41 +1,7 @@ -import * as fs from 'fs' -import * as path from 'path' -import * as mkdir from 'mkdirp' -import * as prettier from 'prettier' -import { compile } from 'art-template' -import del from 'del' - -import * as paths from './config/paths' -import { Entry } from './Entry' -import { Plugin, IPluginFactory } from './Plugin' +import { Plugin } from './Plugin' import { ConfigArgs } from './Server' -const mkd = (dir: string): void => { - try { - fs.lstatSync(dir) - } catch (err) { - mkdir.sync(dir) - } -} - -const format = (raw: string) => - prettier.format(raw, { - semi: false, - singleQuote: true, - trailingComma: 'all', - }) - -const touch = (file: string, raw: string) => { - const content = /js/.test(path.extname(file)) ? format(raw) : raw - - mkd(paths.docz) - fs.writeFileSync(file, content, 'utf-8') -} - -const compiled = (templateFile: string) => - compile(fs.readFileSync(`${paths.templatesPath}/${templateFile}`, 'utf-8')) - -export type TConfigFn = (entries: Entry[]) => C +export type TConfigFn = () => C export type TCompilerFn = (config: C) => Promise export type TServerFn = (compiler: any) => S @@ -50,10 +16,6 @@ export interface IBundlerConstructor extends ICompilerOpts { server: TServerFn } -const app = compiled('app.tpl.js') -const js = compiled('index.tpl.js') -const html = compiled('index.tpl.html') - export class Bundler { readonly id: string readonly args: ConfigArgs @@ -76,60 +38,19 @@ export class Bundler { plugin.bundlerConfig(config, dev) || config } - private mountConfig(entries: Entry[]) { + private mountConfig() { const { plugins, env } = this.args const dev = env === 'development' - const initialConfig = this.config(entries) + const initialConfig = this.config() return Boolean(plugins) && plugins.length > 0 ? plugins.reduce(this.reduceWithPlugins(dev), initialConfig) : initialConfig } - private routesFromEntries(entries: Entry[]) { - return ( - entries && - entries.length > 0 && - entries.reduce((obj, entry) => { - return Object.assign({}, obj, { [entry.name]: entry.route }) - }, {}) - ) - } - - private propOfPlugins(method: keyof IPluginFactory) { - const { plugins } = this.args - return plugins && plugins.map(p => p[method]).filter(m => m) - } - - private generateFilesByTemplate(entries: Entry[]) { - const { theme } = this.args - - touch(paths.indexHtml, html({})) - - touch( - paths.appJs, - app({ - THEME: theme, - ENTRIES: entries, - ROUTES: JSON.stringify(this.routesFromEntries(entries)), - WRAPPERS: this.propOfPlugins('wrapper'), - }) - ) - - touch( - paths.indexJs, - js({ - BEFORE_RENDERS: this.propOfPlugins('beforeRender'), - AFTER_RENDERS: this.propOfPlugins('afterRender'), - }) - ) - } - - public async createCompiler(entries: Entry[]) { - del.sync(paths.docz) - this.generateFilesByTemplate(entries) - return await this.compiler(this.mountConfig(entries)) + public async createCompiler() { + return await this.compiler(this.mountConfig()) } public async createServer(compiler: any): Promise { diff --git a/packages/docz-core/src/Entries.ts b/packages/docz-core/src/Entries.ts index 55c9148..5d23826 100644 --- a/packages/docz-core/src/Entries.ts +++ b/packages/docz-core/src/Entries.ts @@ -1,9 +1,17 @@ import * as glob from 'fast-glob' import * as t from 'babel-types' +import * as fs from 'fs' +import * as path from 'path' +import * as mkdir from 'mkdirp' +import * as prettier from 'prettier' import { NodePath } from 'babel-traverse' +import { compile } from 'art-template' + +import * as paths from './config/paths' import { traverseAndAssign } from './utils/traverse' import { Entry, convertToAst } from './Entry' +import { Plugin, IPluginFactory } from './Plugin' const hasImport = (path: NodePath): boolean => path.isImportDeclaration() && @@ -25,18 +33,82 @@ const checkImport = traverseAndAssign, boolean>( const isFile = (entry: string) => checkImport(convertToAst(entry)) -export class Entries { - private files: string[] - - constructor(pattern: string) { - const ignoreGlob = '!node_modules' - - this.files = glob.sync( - Array.isArray(pattern) ? [...pattern, ignoreGlob] : [pattern, ignoreGlob] - ) - } - - public parse(src: string): Entry[] { - return this.files.filter(isFile).map(file => new Entry({ file, src })) +const mkd = (dir: string): void => { + try { + fs.lstatSync(dir) + } catch (err) { + mkdir.sync(dir) + } +} + +const format = (raw: string) => + prettier.format(raw, { + semi: false, + singleQuote: true, + trailingComma: 'all', + }) + +const touch = (file: string, raw: string) => { + const content = /js/.test(path.extname(file)) ? format(raw) : raw + + mkd(paths.docz) + fs.writeFileSync(file, content, 'utf-8') +} + +const compiled = (file: string) => + compile(fs.readFileSync(path.join(paths.templates, file), 'utf-8')) + +const propOf = (arr: any[], method: keyof IPluginFactory) => + arr && arr.map(p => p[method]).filter(m => m) + +const app = compiled('app.tpl.js') +const js = compiled('index.tpl.js') +const html = compiled('index.tpl.html') + +export interface IGenerateFilesParams { + entries: Entry[] + plugins: Plugin[] + theme: string +} + +export class Entries { + public files: string[] + public all: Entry[] + + constructor(pattern: string, src: string) { + const ignoreGlob = '!node_modules' + const files: string[] = glob.sync( + Array.isArray(pattern) ? [...pattern, ignoreGlob] : [pattern, ignoreGlob] + ) + + this.files = files + this.all = files.filter(isFile).map(file => new Entry({ file, src })) + } + + public map() { + return this.all.reduce((obj: any, entry: Entry) => { + return Object.assign({}, obj, { [entry.filepath]: entry.name }) + }, {}) + } + + static generateFiles({ entries, theme, plugins }: IGenerateFilesParams) { + touch(paths.indexHtml, html({})) + + touch( + paths.appJs, + app({ + THEME: theme, + ENTRIES: entries, + WRAPPERS: propOf(plugins, 'wrapper'), + }) + ) + + touch( + paths.indexJs, + js({ + BEFORE_RENDERS: propOf(plugins, 'beforeRender'), + AFTER_RENDERS: propOf(plugins, 'afterRender'), + }) + ) } } diff --git a/packages/docz-core/src/Entry.ts b/packages/docz-core/src/Entry.ts index f5e917f..1b2afa0 100644 --- a/packages/docz-core/src/Entry.ts +++ b/packages/docz-core/src/Entry.ts @@ -25,18 +25,18 @@ export interface IEntryConstructor { export class Entry { public name: string public filepath: string - public route: string constructor({ src, file }: IEntryConstructor) { const ast = convertToAst(file) const name = getNameFromDoc(ast) || '' - const srcPath = path.resolve(paths.root, src) - const filepath = path.relative(path.relative(paths.root, src), file) - const dir = path.relative(srcPath, path.parse(file).dir) - const route = path.join('/', dir, name) + const filepath = path.relative(paths.root, file) this.name = name - this.route = route this.filepath = filepath } + + static parseName(file: string) { + const ast = convertToAst(file) + return getNameFromDoc(ast) + } } diff --git a/packages/docz-core/src/Server.ts b/packages/docz-core/src/Server.ts index 18f3e82..0122f0e 100644 --- a/packages/docz-core/src/Server.ts +++ b/packages/docz-core/src/Server.ts @@ -1,16 +1,20 @@ import { load } from 'load-cfg' +import { FSWatcher } from 'chokidar' +import * as chokidar from 'chokidar' +import del from 'del' import * as paths from './config/paths' import { pick } from './utils/helpers' +import { Entry } from './Entry' import { Entries } from './Entries' import { Bundler } from './Bundler' import { Plugin } from './Plugin' -process.env['BABEL_ENV'] = process.env['BABEL_ENV'] || 'development' -process.env['NODE_ENV'] = process.env['NODE_ENV'] || 'development' +process.env.BABEL_ENV = process.env.BABEL_ENV || 'development' +process.env.NODE_ENV = process.env.NODE_ENV || 'development' -const ENV = process.env['NODE_ENV'] +const ENV = process.env.NODE_ENV const HOST = process.env.HOST || '0.0.0.0' const PROTOCOL = process.env.HTTPS === 'true' ? 'https' : 'http' @@ -31,30 +35,22 @@ export interface ConfigArgs extends IServerConstructor { } export class Server { - private port: number - private src: string - private plugins: Plugin[] - private entries: Entries + readonly config: ConfigArgs + readonly watcher: FSWatcher private bundler: Bundler constructor(args: IServerConstructor) { const initialArgs = this.getInitialArgs(args) - const { port, theme, files, bundler, src, plugins } = load( - 'docz', - initialArgs - ) + const config = load('docz', initialArgs) - this.port = port - this.src = src - this.plugins = plugins - this.entries = new Entries(files) + this.config = config + this.watcher = chokidar.watch(config.files, { + ignored: /(^|[\/\\])\../, + }) - this.bundler = this.getBundler(bundler).bundler({ - port, + this.bundler = this.getBundler(config.bundler).bundler({ + ...config, paths, - theme, - src, - plugins, env: ENV, host: HOST, protocol: PROTOCOL, @@ -62,7 +58,10 @@ export class Server { } private getInitialArgs(args: IServerConstructor) { - return pick(['port', 'theme', 'files', 'bundler', 'src'], args) + return { + ...pick(['port', 'theme', 'files', 'bundler', 'src'], args), + plugins: [], + } } private getBundler(bundler: string) { @@ -73,11 +72,42 @@ export class Server { } } - public async start() { - const { bundler, entries, plugins } = this + private processEntries(config: ConfigArgs) { + const { files, src, theme, plugins } = config + const cache = new Map() - const compiler = await bundler.createCompiler(entries.parse(this.src)) - const server = await bundler.createServer(compiler) + const generateFilesAndUpdateCache = (entries: Entries) => { + cache.set('map', entries.map()) + Entries.generateFiles({ entries: entries.all, plugins, theme }) + } + + const updateEntries = () => + generateFilesAndUpdateCache(new Entries(files, src)) + + const parseToUpdate = (path: string) => { + const name = Entry.parseName(path) + const entry = cache.get('map')[path] + const newEntries = new Entries(files, src) + + if (name && name !== entry && newEntries.files.includes(path)) { + generateFilesAndUpdateCache(newEntries) + } + } + + this.watcher.on('unlink', updateEntries) + this.watcher.on('change', parseToUpdate) + + generateFilesAndUpdateCache(new Entries(files, src)) + } + + public async start() { + const { plugins, port } = this.config + + del.sync(paths.docz) + this.processEntries(this.config) + + const compiler = await this.bundler.createCompiler() + const server = await this.bundler.createServer(compiler) if (plugins && plugins.length > 0) { for (const plugin of plugins) { @@ -86,6 +116,6 @@ export class Server { } } - server.listen(this.port) + server.listen(port) } }