Skip to content

Commit

Permalink
Merge pull request #58 from bywhitebird/improve-ux-dx
Browse files Browse the repository at this point in the history
Improve ux dx
  • Loading branch information
arthur-fontaine authored Oct 25, 2023
2 parents 21beb8e + 1e3053f commit 32cc57f
Show file tree
Hide file tree
Showing 102 changed files with 2,303 additions and 1,266 deletions.
8 changes: 7 additions & 1 deletion apps/kazam/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"main": "./dist/index.cjs",
"types": "./dist/index.d.ts",
"bin": "./dist/cli.cjs",
"bin": "./dist/cli.mjs",
"files": [
"dist"
],
Expand All @@ -26,13 +26,19 @@
"@whitebird/kaz-ast": "workspace:*",
"@whitebird/kazam-parser-base": "workspace:*",
"@whitebird/kazam-transformer-base": "workspace:*",
"c12": "^1.4.2",
"chalk": "^4",
"commander": "^10.0.1",
"glob": "10.1.0",
"ink": "^4.4.1",
"ink-spinner": "^5.0.0",
"just-kebab-case": "^4.2.0",
"react": "^18.2.0",
"typescript": "^5.0.0",
"zod": "^3.21.4"
},
"devDependencies": {
"@types/ink-spinner": "^3.0.2",
"@types/node": "^18.11.9",
"@vitest/coverage-v8": "^0.34.1",
"@whitebird/kazam-parser-kaz": "workspace:*",
Expand Down
10 changes: 10 additions & 0 deletions apps/kazam/src/adapters/cli/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import process from 'node:process'

import { program } from 'commander'

import { generateCommand } from './commands/generate'

program
.addCommand(generateCommand)

export const runCli = () => program.parse(process.argv)
43 changes: 43 additions & 0 deletions apps/kazam/src/adapters/cli/commands/generate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as fs from 'node:fs'

import { loadConfig } from 'c12'
import { Command } from 'commander'
import { type AppProps, render } from 'ink'
import React from 'react'

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

export const generateCommand = new Command()
.command('generate')
.description('Generate code from your Kazam files')
.option('-c, --config <path>', 'Path to the config file', (path) => {
if (!fs.existsSync(path))
throw new Error(`Could not find config file at ${path}`)

return path
})
.action(async (options) => {
let exit: AppProps['exit'] | undefined
const setExit = (_exit: AppProps['exit']) => exit = _exit

const { config, configFile } = await loadConfig<KazamConfig>({
name: 'kazam',
...(options.config && { configFile: options.config }),
})

if (config === null || configFile === undefined)
throw new Error('Could not load config')

const generatePromise = generate(config, configFile, fs)

render(
<GenerateView
generatePromise={generatePromise}
setExit={setExit}
/>,
)

generatePromise.finally(exit)
})
4 changes: 4 additions & 0 deletions apps/kazam/src/adapters/cli/components/spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Spinner_ from 'ink-spinner'
import React from 'react'

export const Spinner = () => <Spinner_ />
3 changes: 3 additions & 0 deletions apps/kazam/src/adapters/cli/types/Commander.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// declare module 'commander' {
// export * from '@commander-js/extra-typings'
// }
61 changes: 61 additions & 0 deletions apps/kazam/src/adapters/cli/views/generate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import path from 'node:path'

import { type AppProps, Text, useApp } from 'ink'
import React, { useEffect, 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 },
) => {
const { exit } = useApp()

const [status, setStatus] = useState<
| { success: true }
| { error: string }
| { pending: true }
>({ pending: true })
const [writtenPaths, setWrittenPaths] = useState<string[]>([])

useEffect(() => {
setExit?.(exit)

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

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

if ('success' in status) {
return <>
<Text><Text color="green"></Text> Successfully generated components</Text>
{writtenPaths.map(writtenPath =>
<Text key={writtenPath} color='gray'>{' '}{path.normalize(writtenPath)}</Text>,
)}
</>
}

if ('error' in status) {
return <>
<Text><Text color="red"></Text> {status.error}</Text>
</>
}

return <>
<Text>
<Text color="blue">
<Spinner />
</Text>
{' Generating components...'}
</Text>
</>
}
4 changes: 4 additions & 0 deletions apps/kazam/src/adapters/lib/define-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineConfig as defineConfig_ } from '../../application/usecases/define-config'
import type { UserConfig } from '../../types/kazam-config'

export const defineConfig = (config: UserConfig) => defineConfig_(config)
9 changes: 9 additions & 0 deletions apps/kazam/src/adapters/lib/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type * as fs from 'node:fs'

import { generate as generate_ } from '../../application/usecases/generate'
import type { KazamConfig } from '../../types/kazam-config'

export const generate = (
{ rootDir, ...config }: KazamConfig & { rootDir: string },
fileSystem?: typeof fs | undefined,
) => generate_(config, rootDir, fileSystem)
2 changes: 2 additions & 0 deletions apps/kazam/src/adapters/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { defineConfig } from './define-config'
export { generate } from './generate'
4 changes: 4 additions & 0 deletions apps/kazam/src/application/usecases/define-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { kazamConfigSchema } from '../../core/schemas/kazam-config'
import type { UserConfig } from '../../types/kazam-config'

export const defineConfig = (config: UserConfig) => kazamConfigSchema.parse(config)
102 changes: 102 additions & 0 deletions apps/kazam/src/application/usecases/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type * as fs from 'node:fs'

import type { TransformerOutput } from '@whitebird/kazam-transformer-base'
import kebabCase from 'just-kebab-case'

import { generateEvents } from '../../core/events/generate'
import type { KazamConfig } from '../../types/kazam-config'
import type { Transformer } from '../../types/transformer'

const writeResults = (
files: Map<string, string>,
fileSystem: typeof fs,
) => {
files.forEach((fileContents, filePath) => {
const directoryPath = filePath.split('/').slice(0, -1).join('/')

fileSystem.mkdirSync(directoryPath, { recursive: true })
fileSystem.writeFileSync(filePath, fileContents)

generateEvents.emit('file-written', filePath)
})
}

const formatResults = (
transformerResult: TransformerOutput<{ outputFileNameFormat: string }>,
transformer: Transformer,
config: Exclude<KazamConfig, unknown[]>,
): Parameters<typeof writeResults>[0] => {
const formattedTransformerResult = new Map<string, string>()

const transformerDirectory = [
config.output,
kebabCase(transformer.name.replace(/^Transformer/, '')),
].join('/')

transformerResult.forEach(({ filePath, content }, sourceFilePath) => {
const sourceExtension = `.${sourceFilePath.split('.').slice(-1)[0]}` ?? ''
const transformedExtension = `.${filePath.split('.').slice(-1)[0]}` ?? ''

const outputFilePath = [
transformerDirectory,
// The following line replaces `.kaz.tsx` with `.tsx` (for example)
filePath.replace(
new RegExp(`${sourceExtension.replace('.', '\\.')}${transformedExtension.replace('.', '\\.')}$`),
transformedExtension,
),
].join('/')

formattedTransformerResult.set(outputFilePath, content)
})

return formattedTransformerResult
}

const generateForConfig = async (
config: Exclude<KazamConfig, unknown[]>,
configPath: string,
fileSystem?: typeof fs | undefined,
) => {
return Promise.all(
config.parsers.map(async (ParserClass) => {
const parser = new ParserClass()

const transformerInput = await parser.loadAndParse({
...config,
configPath,
})

return config.transformers.map((TransformerClass) => {
const transformer = new TransformerClass(transformerInput, {})

const transformerResult = transformer.transform()

if (fileSystem) {
writeResults(
formatResults(transformerResult, TransformerClass, config),
fileSystem,
)
}

return transformerResult
})
}),
)
.then(results => results.flat())
}

export const generate = async (
config: KazamConfig,
configPath: string,
fileSystem?: typeof fs | undefined,
) => {
if (!Array.isArray(config))
config = [config]

const results = await Promise.all(
config.map(config => generateForConfig(config, configPath, fileSystem)),
)
.then(results => results.flat())

return results
}
13 changes: 2 additions & 11 deletions apps/kazam/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
#!/usr/bin/env node
import { runCli } from './adapters/cli/cli'

import process from 'node:process'

import { program } from 'commander'

import { generateCommand } from './commands/generate'

program
.addCommand(generateCommand)

program.parse(process.argv)
runCli()
33 changes: 0 additions & 33 deletions apps/kazam/src/commands/generate.ts

This file was deleted.

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

const generateEvents = registerEventEmitter<{
'file-written': string
}>()

export { generateEvents }
23 changes: 23 additions & 0 deletions apps/kazam/src/core/lib/typed-event-emitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { EventEmitter } from 'node:events'

class ReturnedEmitter<Events extends Record<string, unknown>> {
#eventEmitter = new EventEmitter()

emit<EventName extends keyof Events>(eventName: EventName extends string ? EventName : never, data: Events[EventName]) {
return this.#eventEmitter.emit(eventName, data)
}

on<EventName extends keyof Events>(eventName: EventName extends string ? EventName : never, listener: (data: Events[EventName]) => void): this {
this.#eventEmitter.on(eventName, listener)
return this
}

once<EventName extends keyof Events>(eventName: EventName extends string ? EventName : never, listener: (data: Events[EventName]) => void): this {
this.#eventEmitter.once(eventName, listener)
return this
}
}

export const registerEventEmitter = <Events extends Record<string, unknown>>() => {
return new ReturnedEmitter<Events>()
}
28 changes: 28 additions & 0 deletions apps/kazam/src/core/schemas/kazam-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ParserBase } from '@whitebird/kazam-parser-base'
import { TransformerBase } from '@whitebird/kazam-transformer-base'
import { z } from 'zod'

import type { Parser } from '../../types/parser'
import type { Transformer } from '../../types/transformer'

const transformerSchema = z.custom<Transformer>(
v => typeof v === 'function' && v.prototype instanceof TransformerBase,
{ message: 'Must be an instance of TransformerBase' },
)

const parserSchema = z.custom<Parser>(
v => typeof v === 'function' && v.prototype instanceof ParserBase,
{ message: 'Must be an instance of ParserBase' },
)

const singleKazamConfigSchema = z.object({
input: z.string().array(),
output: z.string(),
transformers: z.array(transformerSchema),
parsers: z.array(parserSchema),
})

export const kazamConfigSchema = z.union([
singleKazamConfigSchema,
z.array(singleKazamConfigSchema),
])
Loading

0 comments on commit 32cc57f

Please sign in to comment.