Skip to content

Commit

Permalink
feat(cli): support yaml config file
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Apr 14, 2021
1 parent 3d4ebb8 commit 33ca25e
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 37 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"scripts": {
"atri": "yarn workspace atri",
"addons": "yarn workspace addons",
"cli": "yarn workspace test koishi",
"cli": "yarn compile koishi && yarn workspace test koishi",
"build": "yarn compile && yarn dtsc",
"build:ci": "yarn build --listEmittedFiles && yarn build:web",
"build:web": "node -r ./build/register packages/plugin-webui/build",
Expand Down
2 changes: 2 additions & 0 deletions packages/koishi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@
"koishi"
],
"devDependencies": {
"@types/js-yaml": "^4.0.0",
"@types/prompts": "^2.0.10"
},
"dependencies": {
"cac": "^6.7.2",
"chokidar": "^3.5.1",
"js-yaml": "^4.0.0",
"kleur": "^4.1.4",
"koishi-core": "^3.8.0",
"koishi-utils": "^4.2.0",
Expand Down
36 changes: 23 additions & 13 deletions packages/koishi/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ async function createConfig() {
}
}

const sourceTypes = ['js', 'ts', 'json'] as const
const sourceTypes = ['js', 'ts', 'json', 'yaml', 'yml'] as const
type SourceType = typeof sourceTypes[number]

const error = red('error')
Expand All @@ -256,8 +256,9 @@ type Serializable = string | number | Serializable[] | SerializableObject
function joinLines(lines: string[], type: SourceType, indent: string) {
if (!lines.length) return ''
// 如果是根节点就多个换行,看着舒服
const separator = indent ? ',\n ' + indent : ',\n\n '
return `\n ${indent}${lines.join(separator)}${type === 'json' ? '' : ','}\n${indent}`
let separator = '\n ' + indent
if (type !== 'yaml') separator = ',' + separator
return `\n ${indent}${lines.join(separator)}${type === 'json' || type === 'yaml' ? '' : ','}\n${indent}`
}

function comment(data: SerializableObject, prop: string) {
Expand All @@ -273,34 +274,41 @@ function comment(data: SerializableObject, prop: string) {
}
}

function codegen(data: Serializable, type: SourceType, indent = '') {
function codegen(data: Serializable, type: SourceType, indent = ''): string {
if (data === null) return 'null'

switch (typeof data) {
case 'number': case 'boolean': return '' + data
case 'string': return type === 'json' || data.includes("'") && !data.includes('"')
? `"${data.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
: `'${data.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
case 'string': return type === 'yaml' ? data
: type === 'json' || data.includes("'") && !data.includes('"')
? `"${data.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
: `'${data.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
case 'undefined': return undefined
}

if (Array.isArray(data)) {
return `[${data.map(value => codegen(value, type, indent)).join(', ')}]`
// return `[${joinLines(value.map(value => codegen(value, type, ' ' + indent)), type, indent)}]`
return type === 'yaml'
? joinLines(data.map(value => '- ' + codegen(value, type, ' ' + indent).trimStart()), type, indent)
: `[${data.map(value => codegen(value, type, indent)).join(', ')}]`
}

return `{${joinLines(Object.entries(data).filter(([, value]) => value !== undefined).map(([key, value]) => {
// object
const prefix = type === 'yaml' ? '# ' : '// '
const output = joinLines(Object.entries(data).filter(([, value]) => value !== undefined).map(([key, value]) => {
let output = type !== 'json' && comment(data, key) || ''
if (output) output = output.split('\n').map(line => '// ' + line + '\n ' + indent).join('')
if (output) output = output.split('\n').map(line => prefix + line + '\n ' + indent).join('')
output += type === 'json' ? `"${key}"` : key
output += ': ' + codegen(value, type, ' ' + indent)
return output
}), type, indent)}}`
}), type, indent)
return type === 'yaml' ? output : `{${output}}`
}

const rootComment = '配置项文档:https://koishi.js.org/api/app.html'

async function writeConfig(config: any, path: string, type: SourceType) {
if (type === 'yml') type = 'yaml'

// generate output
let output = codegen(config, type) + '\n'
if (type === 'js') {
Expand All @@ -310,12 +318,14 @@ async function writeConfig(config: any, path: string, type: SourceType) {
+ rootComment
+ '\nexport default '
+ output.replace(/\n$/, ' as AppConfig\n')
} else if (type === 'yaml') {
output = '# ' + rootComment + '\n' + output.replace(/^ {2}/mg, '')
}

// write to file
const folder = dirname(path)
await fs.mkdir(folder, { recursive: true })
await fs.writeFile(path, output)
await fs.writeFile(path, output.replace(/^ +$/mg, ''))
console.log(`${success} created config file: ${path}`)
}

Expand Down
48 changes: 25 additions & 23 deletions packages/koishi/src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { App, BotOptions, Plugin, version } from 'koishi-core'
import { resolve, dirname, relative } from 'path'
import { resolve, relative, extname, dirname } from 'path'
import { coerce, Logger, noop, LogLevelConfig } from 'koishi-utils'
import { readFileSync, readdirSync } from 'fs'
import { performance } from 'perf_hooks'
import { watch } from 'chokidar'
import { yellow } from 'kleur'
import { AppConfig } from '..'

const logger = new Logger('app')
const cwd = process.cwd()
let configDir = process.cwd()

function handleException(error: any) {
logger.error(error)
Expand All @@ -16,30 +16,31 @@ function handleException(error: any) {

process.on('uncaughtException', handleException)

const configFile = resolve(cwd, process.env.KOISHI_CONFIG_FILE || 'koishi.config')
const configDir = dirname(configFile)

function isErrorModule(error: any) {
return error.code !== 'MODULE_NOT_FOUND' || error.requireStack && error.requireStack[0] !== __filename
}

function tryCallback<T>(callback: () => T) {
try {
return callback()
} catch (error) {
if (isErrorModule(error) && error.code !== 'ENOENT') {
throw error
}
let config: AppConfig, configFile: string, configExt: string
const basename = 'koishi.config'
if (process.env.KOISHI_CONFIG_FILE) {
configFile = resolve(configDir, process.env.KOISHI_CONFIG_FILE)
configExt = extname(configFile)
configDir = dirname(configFile)
} else {
const files = readdirSync(configDir)
const ext = ['.js', '.json', '.ts', '.yaml', '.yml'].find(ext => files.includes(basename + ext))
if (!ext) {
throw new Error(`config file not found. use ${yellow('koishi init')} command to initialize a config file.`)
}
configFile = configDir + '/' + basename + ext
}

const config: AppConfig = tryCallback(() => {
if (['.yaml', '.yml'].includes(configExt)) {
const { load } = require('js-yaml') as typeof import('js-yaml')
config = load(readFileSync(configFile, 'utf8')) as any
} else {
const exports = require(configFile)
return exports.__esModule ? exports.default : exports
})
config = exports.__esModule ? exports.default : exports
}

if (!config) {
throw new Error(`config file not found. use ${yellow('koishi init')} command to initialize a config file.`)
function isErrorModule(error: any) {
return error.code !== 'MODULE_NOT_FOUND' || error.requireStack && error.requireStack[0] !== __filename
}

function loadEcosystem(type: string, name: string) {
Expand Down Expand Up @@ -193,8 +194,9 @@ function loadDependencies(filename: string, ignored: Set<string>) {
function createWatcher() {
if (process.env.KOISHI_WATCH_ROOT === undefined && !config.watch) return

const { watch } = require('chokidar') as typeof import('chokidar')
const { root = '', ignored = [], fullReload } = config.watch || {}
const watchRoot = resolve(cwd, process.env.KOISHI_WATCH_ROOT ?? root)
const watchRoot = resolve(configDir, process.env.KOISHI_WATCH_ROOT ?? root)
const watcher = watch(watchRoot, {
...config.watch,
ignored: ['**/node_modules/**', '**/.git/**', ...ignored],
Expand Down

0 comments on commit 33ca25e

Please sign in to comment.