Skip to content

Commit

Permalink
feat(Kazam): add watcher mode
Browse files Browse the repository at this point in the history
  • Loading branch information
arthur-fontaine committed Oct 28, 2023
1 parent 04478cc commit 6263eba
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 19 deletions.
1 change: 1 addition & 0 deletions apps/kazam/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@whitebird/kazam-transformer-base": "workspace:*",
"c12": "^1.4.2",
"chalk": "^4",
"chokidar": "^3.5.3",
"commander": "^10.0.1",
"glob": "10.1.0",
"ink": "^4.4.1",
Expand Down
7 changes: 5 additions & 2 deletions apps/kazam/src/adapters/cli/commands/generate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { type AppProps, render } from 'ink'
import React from 'react'

import { generate } from '../../../application/usecases/generate'
import { generateWatch } from '../../../application/usecases/generate-watch'
import type { KazamConfig } from '../../../types/kazam-config'
import { GenerateView } from '../views/generate'

Expand All @@ -18,6 +19,7 @@ export const generateCommand = new Command()

return path
})
.option('-w, --watch', 'Watch for changes and regenerate code')
.action(async (options) => {
let exit: AppProps['exit'] | undefined
const setExit = (_exit: AppProps['exit']) => exit = _exit
Expand All @@ -30,11 +32,12 @@ export const generateCommand = new Command()
if (config === null || configFile === undefined)
throw new Error('Could not load config')

const generatePromise = generate(config, configFile, fs)
const generatePromise = options.watch === true
? generateWatch(config, configFile, fs)
: generate(config, configFile, fs)

render(
<GenerateView
generatePromise={generatePromise}
setExit={setExit}
/>,
)
Expand Down
6 changes: 3 additions & 3 deletions apps/kazam/src/adapters/cli/types/Commander.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// declare module 'commander' {
// export * from '@commander-js/extra-typings'
// }
declare module 'commander' {
export * from '@commander-js/extra-typings'
}
40 changes: 27 additions & 13 deletions apps/kazam/src/adapters/cli/views/generate.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,54 @@
import path from 'node:path'

import { type AppProps, Text, useApp } from 'ink'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useLayoutEffect, useState } from 'react'

import type { generate } from '../../../application/usecases/generate'
import { generateEvents } from '../../../core/events/generate'
import { Spinner } from '../components/spinner'

export const GenerateView = (
{ generatePromise, setExit }:
{ generatePromise: ReturnType<typeof generate>; setExit?: (exit: AppProps['exit']) => void },
{ setExit }:
{ setExit?: (exit: AppProps['exit']) => void },
) => {
const { exit } = useApp()

const [status, setStatus] = useState<
| { success: true }
| { error: string }
| { pending: true }
>({ pending: true })
>(
generateEvents.hasReceived('success')
? { success: true }
: generateEvents.hasReceived('error')
? { error: generateEvents.getLatestReceived('error')!.message }
: { pending: true },
)
const [writtenPaths, setWrittenPaths] = useState<string[]>([])

useEffect(() => {
setExit?.(exit)
useLayoutEffect(() => {
generateEvents.on('pending', () => {
setStatus({ pending: true })
setWrittenPaths([])
})

generatePromise
.then(() => setStatus({ success: true }))
.catch((error) => {
setStatus({ error })
console.error('ERROR', error)
})
generateEvents.on('success', () => {
setStatus({ success: true })
})

generateEvents.on('error', (error) => {
setStatus({ error: error.message })
console.error('ERROR', error)
})

generateEvents.on('file-written', filePath =>
setWrittenPaths(writtenPaths => [...writtenPaths, filePath]),
)
}, [])

useEffect(() => {
setExit?.(exit)
}, [])

if ('success' in status) {
return <>
<Text><Text color="green"></Text> Successfully generated components</Text>
Expand Down
32 changes: 32 additions & 0 deletions apps/kazam/src/application/usecases/generate-watch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import chokidar from 'chokidar'

import { generate } from './generate'

const getFilesToWatch = ([config, configPath]: Parameters<typeof generate>): string[] => {
if (!Array.isArray(config))
config = [config]

return [
...config.flatMap(config => config.input),
configPath,
]
}

export const generateWatch = (...argsGenerate: Parameters<typeof generate>) => {
return new Promise<void>((_resolve, reject) => {
// Generate once before watching
generate(...argsGenerate)

const watcher = chokidar.watch(getFilesToWatch(argsGenerate), {
persistent: true,
})

watcher.on('change', async () => {
await generate(...argsGenerate)
})

watcher.on('error', (error) => {
reject(error)
})
})
}
10 changes: 10 additions & 0 deletions apps/kazam/src/application/usecases/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,23 @@ export const generate = async (
configPath: string,
fileSystem?: typeof fs | undefined,
) => {
generateEvents.emit('pending', undefined)

if (!Array.isArray(config))
config = [config]

const results = await Promise.all(
config.map(config => generateForConfig(config, configPath, fileSystem)),
)
.then(results => results.flat())
.then((results) => {
generateEvents.emit('success', undefined)
return results
})
.catch((error) => {
generateEvents.emit('error', error)
throw error
})

return results
}
3 changes: 3 additions & 0 deletions apps/kazam/src/core/events/generate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { registerEventEmitter } from '../lib/typed-event-emitter'

const generateEvents = registerEventEmitter<{
'pending': void
'success': void
'error': Error
'file-written': string
}>()

Expand Down
15 changes: 15 additions & 0 deletions apps/kazam/src/core/lib/typed-event-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { EventEmitter } from 'node:events'
class ReturnedEmitter<Events extends Record<string, unknown>> {
#eventEmitter = new EventEmitter()

#receivedEvents: { [EventName in keyof Events]?: Events[EventName][] } = {}

emit<EventName extends keyof Events>(eventName: EventName extends string ? EventName : never, data: Events[EventName]) {
if (!this.#receivedEvents[eventName])
this.#receivedEvents[eventName] = []

this.#receivedEvents[eventName]?.push(data as never)

return this.#eventEmitter.emit(eventName, data)
}

Expand All @@ -16,6 +23,14 @@ class ReturnedEmitter<Events extends Record<string, unknown>> {
this.#eventEmitter.once(eventName, listener)
return this
}

hasReceived<EventName extends keyof Events>(eventName: EventName extends string ? EventName : never): boolean {
return eventName in this.#receivedEvents
}

getLatestReceived<EventName extends keyof Events>(eventName: EventName extends string ? EventName : never): Events[EventName] | undefined {
return this.#receivedEvents[eventName]?.at(-1)
}
}

export const registerEventEmitter = <Events extends Record<string, unknown>>() => {
Expand Down
Loading

0 comments on commit 6263eba

Please sign in to comment.