diff --git a/apps/kazam/package.json b/apps/kazam/package.json
index 8a45741..7f12bd8 100644
--- a/apps/kazam/package.json
+++ b/apps/kazam/package.json
@@ -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",
diff --git a/apps/kazam/src/adapters/cli/commands/generate.tsx b/apps/kazam/src/adapters/cli/commands/generate.tsx
index b61b412..18264fe 100644
--- a/apps/kazam/src/adapters/cli/commands/generate.tsx
+++ b/apps/kazam/src/adapters/cli/commands/generate.tsx
@@ -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'
@@ -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
@@ -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(
,
)
diff --git a/apps/kazam/src/adapters/cli/types/Commander.d.ts b/apps/kazam/src/adapters/cli/types/Commander.d.ts
index fef67b9..c55a0be 100644
--- a/apps/kazam/src/adapters/cli/types/Commander.d.ts
+++ b/apps/kazam/src/adapters/cli/types/Commander.d.ts
@@ -1,3 +1,3 @@
-// declare module 'commander' {
-// export * from '@commander-js/extra-typings'
-// }
+declare module 'commander' {
+ export * from '@commander-js/extra-typings'
+}
diff --git a/apps/kazam/src/adapters/cli/views/generate.tsx b/apps/kazam/src/adapters/cli/views/generate.tsx
index 29d2365..f41078c 100644
--- a/apps/kazam/src/adapters/cli/views/generate.tsx
+++ b/apps/kazam/src/adapters/cli/views/generate.tsx
@@ -1,15 +1,14 @@
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; setExit?: (exit: AppProps['exit']) => void },
+ { setExit }:
+ { setExit?: (exit: AppProps['exit']) => void },
) => {
const { exit } = useApp()
@@ -17,24 +16,39 @@ export const GenerateView = (
| { 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([])
- 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 <>
✔ Successfully generated components
diff --git a/apps/kazam/src/application/usecases/generate-watch.ts b/apps/kazam/src/application/usecases/generate-watch.ts
new file mode 100644
index 0000000..0c42e90
--- /dev/null
+++ b/apps/kazam/src/application/usecases/generate-watch.ts
@@ -0,0 +1,32 @@
+import chokidar from 'chokidar'
+
+import { generate } from './generate'
+
+const getFilesToWatch = ([config, configPath]: Parameters): string[] => {
+ if (!Array.isArray(config))
+ config = [config]
+
+ return [
+ ...config.flatMap(config => config.input),
+ configPath,
+ ]
+}
+
+export const generateWatch = (...argsGenerate: Parameters) => {
+ return new Promise((_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)
+ })
+ })
+}
diff --git a/apps/kazam/src/application/usecases/generate.ts b/apps/kazam/src/application/usecases/generate.ts
index fdfa0d2..0c803c3 100644
--- a/apps/kazam/src/application/usecases/generate.ts
+++ b/apps/kazam/src/application/usecases/generate.ts
@@ -120,6 +120,8 @@ export const generate = async (
configPath: string,
fileSystem?: typeof fs | undefined,
) => {
+ generateEvents.emit('pending', undefined)
+
if (!Array.isArray(config))
config = [config]
@@ -127,6 +129,14 @@ export const generate = async (
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
}
diff --git a/apps/kazam/src/core/events/generate.ts b/apps/kazam/src/core/events/generate.ts
index 18c6b10..b7865cf 100644
--- a/apps/kazam/src/core/events/generate.ts
+++ b/apps/kazam/src/core/events/generate.ts
@@ -1,6 +1,9 @@
import { registerEventEmitter } from '../lib/typed-event-emitter'
const generateEvents = registerEventEmitter<{
+ 'pending': void
+ 'success': void
+ 'error': Error
'file-written': string
}>()
diff --git a/apps/kazam/src/core/lib/typed-event-emitter.ts b/apps/kazam/src/core/lib/typed-event-emitter.ts
index 585494f..5736663 100644
--- a/apps/kazam/src/core/lib/typed-event-emitter.ts
+++ b/apps/kazam/src/core/lib/typed-event-emitter.ts
@@ -3,7 +3,14 @@ import { EventEmitter } from 'node:events'
class ReturnedEmitter> {
#eventEmitter = new EventEmitter()
+ #receivedEvents: { [EventName in keyof Events]?: Events[EventName][] } = {}
+
emit(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)
}
@@ -16,6 +23,14 @@ class ReturnedEmitter> {
this.#eventEmitter.once(eventName, listener)
return this
}
+
+ hasReceived(eventName: EventName extends string ? EventName : never): boolean {
+ return eventName in this.#receivedEvents
+ }
+
+ getLatestReceived(eventName: EventName extends string ? EventName : never): Events[EventName] | undefined {
+ return this.#receivedEvents[eventName]?.at(-1)
+ }
}
export const registerEventEmitter = >() => {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1a7b619..2e3ec57 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -214,6 +214,9 @@ importers:
chalk:
specifier: ^4
version: 4.1.2
+ chokidar:
+ specifier: ^3.5.3
+ version: 3.5.3
commander:
specifier: ^10.0.1
version: 10.0.1
@@ -283,7 +286,7 @@ importers:
version: 7.5.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)
'@storybook/react-vite':
specifier: ^7.5.1
- version: 7.5.1(patch_hash=gdcv4ovpvjyowkf3z6yzn7rrom)(react-dom@18.2.0)(react@18.2.0)(rollup@3.29.3)(typescript@5.2.2)(vite@4.4.9)
+ version: 7.5.1(patch_hash=gdcv4ovpvjyowkf3z6yzn7rrom)(react-dom@18.2.0)(react@18.2.0)(rollup@3.29.3)(typescript@5.2.2)(vite@4.5.0)
'@storybook/testing-library':
specifier: ^0.2.2
version: 0.2.2
@@ -3151,6 +3154,24 @@ packages:
react-docgen-typescript: 2.2.2(typescript@5.2.2)
typescript: 5.2.2
vite: 4.4.9(@types/node@18.18.0)
+ dev: false
+
+ /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.2.2)(vite@4.5.0):
+ resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==}
+ peerDependencies:
+ typescript: '>= 4.3.x'
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ glob: 7.2.3
+ glob-promise: 4.2.2(glob@7.2.3)
+ magic-string: 0.27.0
+ react-docgen-typescript: 2.2.2(typescript@5.2.2)
+ typescript: 5.2.2
+ vite: 4.5.0(@types/node@20.4.7)
+ dev: true
/@jridgewell/gen-mapping@0.3.3:
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
@@ -4468,6 +4489,45 @@ packages:
transitivePeerDependencies:
- encoding
- supports-color
+ dev: false
+
+ /@storybook/builder-vite@7.5.1(typescript@5.2.2)(vite@4.5.0):
+ resolution: {integrity: sha512-fsF4LsxroVvjBJoI5AvRA6euhpYrb5euii5kPzrsWXLOn6gDBK0jQ0looep/io7J45MisDjRTPp14A02pi1bkw==}
+ peerDependencies:
+ '@preact/preset-vite': '*'
+ typescript: '>= 4.3.x'
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0
+ vite-plugin-glimmerx: '*'
+ peerDependenciesMeta:
+ '@preact/preset-vite':
+ optional: true
+ typescript:
+ optional: true
+ vite-plugin-glimmerx:
+ optional: true
+ dependencies:
+ '@storybook/channels': 7.5.1
+ '@storybook/client-logger': 7.5.1
+ '@storybook/core-common': 7.5.1
+ '@storybook/csf-plugin': 7.5.1
+ '@storybook/node-logger': 7.5.1
+ '@storybook/preview': 7.5.1
+ '@storybook/preview-api': 7.5.1
+ '@storybook/types': 7.5.1
+ '@types/find-cache-dir': 3.2.1
+ browser-assert: 1.2.1
+ es-module-lexer: 0.9.3
+ express: 4.18.2
+ find-cache-dir: 3.3.2
+ fs-extra: 11.1.1
+ magic-string: 0.30.3
+ rollup: 3.29.3
+ typescript: 5.2.2
+ vite: 4.5.0(@types/node@20.4.7)
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+ dev: true
/@storybook/channels@7.5.1:
resolution: {integrity: sha512-7hTGHqvtdFTqRx8LuCznOpqPBYfUeMUt/0IIp7SFuZT585yMPxrYoaK//QmLEWnPb80B8HVTSQi7caUkJb32LA==}
@@ -4825,6 +4885,35 @@ packages:
- supports-color
- typescript
- vite-plugin-glimmerx
+ dev: false
+ patched: true
+
+ /@storybook/react-vite@7.5.1(patch_hash=gdcv4ovpvjyowkf3z6yzn7rrom)(react-dom@18.2.0)(react@18.2.0)(rollup@3.29.3)(typescript@5.2.2)(vite@4.5.0):
+ resolution: {integrity: sha512-996/CtOqTjDWMKBGcHG8pwIVlORnoknLD+OTkPXl+aAl9oM9jUtc7psVKLJKGHSHTlVElM2wMTwIHnJ4yeP7bw==}
+ engines: {node: '>=16'}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0
+ dependencies:
+ '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.2.2)(vite@4.5.0)
+ '@rollup/pluginutils': 5.0.4(rollup@3.29.3)
+ '@storybook/builder-vite': 7.5.1(typescript@5.2.2)(vite@4.5.0)
+ '@storybook/react': 7.5.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)
+ '@vitejs/plugin-react': 3.1.0(vite@4.5.0)
+ magic-string: 0.30.3
+ react: 18.2.0
+ react-docgen: 6.0.4
+ react-dom: 18.2.0(react@18.2.0)
+ vite: 4.5.0(@types/node@20.4.7)
+ transitivePeerDependencies:
+ - '@preact/preset-vite'
+ - encoding
+ - rollup
+ - supports-color
+ - typescript
+ - vite-plugin-glimmerx
+ dev: true
patched: true
/@storybook/react@7.5.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2):
@@ -5580,6 +5669,23 @@ packages:
vite: 4.4.9(@types/node@18.18.0)
transitivePeerDependencies:
- supports-color
+ dev: false
+
+ /@vitejs/plugin-react@3.1.0(vite@4.5.0):
+ resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ vite: ^4.1.0-beta.0
+ dependencies:
+ '@babel/core': 7.23.0
+ '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.23.0)
+ '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.23.0)
+ magic-string: 0.27.0
+ react-refresh: 0.14.0
+ vite: 4.5.0(@types/node@20.4.7)
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
/@vitejs/plugin-vue@4.3.4(vite@4.4.9)(vue@3.3.4):
resolution: {integrity: sha512-ciXNIHKPriERBisHFBvnTbfKa6r9SAesOYXeGDzgegcvy9Q4xdScSHAmKbNT0M3O0S9LKhIf5/G+UYG4NnnzYw==}
@@ -12023,6 +12129,15 @@ packages:
picocolors: 1.0.0
source-map-js: 1.0.2
+ /postcss@8.4.31:
+ resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
+ engines: {node: ^10 || ^12 || >=14}
+ dependencies:
+ nanoid: 3.3.6
+ picocolors: 1.0.0
+ source-map-js: 1.0.2
+ dev: true
+
/prebuild-install@7.1.1:
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
engines: {node: '>=10'}
@@ -12810,6 +12925,14 @@ packages:
optionalDependencies:
fsevents: 2.3.3
+ /rollup@3.29.4:
+ resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
+ engines: {node: '>=14.18.0', npm: '>=8.0.0'}
+ hasBin: true
+ optionalDependencies:
+ fsevents: 2.3.3
+ dev: true
+
/rollup@4.0.2:
resolution: {integrity: sha512-MCScu4usMPCeVFaiLcgMDaBQeYi1z6vpWxz0r0hq0Hv77Y2YuOTZldkuNJ54BdYBH3e+nkrk6j0Rre/NLDBYzg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -14430,6 +14553,42 @@ packages:
fsevents: 2.3.3
dev: false
+ /vite@4.5.0(@types/node@20.4.7):
+ resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': '>= 14'
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ dependencies:
+ '@types/node': 20.4.7
+ esbuild: 0.18.20
+ postcss: 8.4.31
+ rollup: 3.29.4
+ optionalDependencies:
+ fsevents: 2.3.3
+ dev: true
+
/vitefu@0.2.4(vite@4.4.9):
resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
peerDependencies: