feat(docz-core): add chokidar to watch new entries

This commit is contained in:
Pedro Nauck
2018-04-16 13:07:18 -03:00
parent 206a67fb32
commit 2f073d5477
4 changed files with 153 additions and 130 deletions

View File

@@ -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<C> = (entries: Entry[]) => C
export type TConfigFn<C> = () => C
export type TCompilerFn<C> = (config: C) => Promise<any>
export type TServerFn<S> = (compiler: any) => S
@@ -50,10 +16,6 @@ export interface IBundlerConstructor<C, S> extends ICompilerOpts {
server: TServerFn<S>
}
const app = compiled('app.tpl.js')
const js = compiled('index.tpl.js')
const html = compiled('index.tpl.html')
export class Bundler<C = any, S = any> {
readonly id: string
readonly args: ConfigArgs
@@ -76,60 +38,19 @@ export class Bundler<C = any, S = any> {
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<S> {

View File

@@ -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<any>): boolean =>
path.isImportDeclaration() &&
@@ -25,18 +33,82 @@ const checkImport = traverseAndAssign<NodePath<t.Node>, 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'),
})
)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}