diff --git a/packages/cli/build/compile.ts b/packages/cli/build/compile.ts index f6af998e40..600a71f5e5 100644 --- a/packages/cli/build/compile.ts +++ b/packages/cli/build/compile.ts @@ -1,5 +1,16 @@ import { defineBuild } from '../../../build' export = defineBuild(async (base, options) => { - options.entryPoints.push(base + '/src/worker.ts') + options.plugins = [{ + name: 'external library', + setup(build) { + build.onResolve({ filter: /^([@/\w-]+|.+\/utils)$/ }, (a) => ({ external: true })) + }, + }] + + options.entryPoints = [ + base + '/src/bin.ts', + base + '/src/utils.ts', + base + '/src/worker/index.ts', + ] }) diff --git a/packages/cli/index.d.ts b/packages/cli/index.d.ts deleted file mode 100644 index 9b15121114..0000000000 --- a/packages/cli/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { App } from 'koishi' - -export * from 'koishi' - -export type * from './lib/addons' -export type * from './lib/loader' -export type * from './lib/worker' - -export function defineConfig(config: App.Config): App.Config diff --git a/packages/cli/index.js b/packages/cli/index.js deleted file mode 100644 index e2989250e9..0000000000 --- a/packages/cli/index.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = require('koishi') - -module.exports.defineConfig = function defineConfig(config) { - return config -} diff --git a/packages/cli/package.json b/packages/cli/package.json index c2348bb029..8ae7b37782 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -2,18 +2,16 @@ "name": "@koishijs/cli", "description": "CLI for Koishi", "version": "4.2.2", - "main": "index.js", - "typings": "index.d.ts", + "main": "lib/utils.js", + "typings": "lib/utils.d.ts", "engines": { "node": ">=12.0.0" }, "files": [ - "lib", - "index.js", - "index.d.ts" + "lib" ], "bin": { - "koishi": "lib/index.js" + "koishi": "lib/bin.js" }, "author": "Shigma ", "license": "MIT", @@ -33,6 +31,9 @@ "chatbot", "koishi" ], + "peerDependencies": { + "koishi": "^4.2.2" + }, "devDependencies": { "@types/js-yaml": "^4.0.5", "@types/prompts": "^2.0.14", @@ -44,7 +45,6 @@ "dotenv": "^16.0.0", "js-yaml": "^4.1.0", "kleur": "^4.1.4", - "koishi": "^4.2.2", "prompts": "^2.4.2", "throttle-debounce": "^3.0.1" } diff --git a/packages/cli/src/addons/index.ts b/packages/cli/src/addons/index.ts deleted file mode 100644 index 57a1d49eed..0000000000 --- a/packages/cli/src/addons/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { App, Context, Dict, Schema, Time } from 'koishi' -import * as daemon from './daemon' -import * as logger from './logger' -import Watcher from './watcher' - -declare module 'koishi' { - interface App { - prologue: string[] - watcher: Watcher - } - - namespace App { - interface Config extends daemon.Config { - plugins?: Dict - timezoneOffset?: number - stackTraceLimit?: number - logger?: logger.Config - watch?: Watcher.Config - } - } -} - -Object.assign(App.Config.Advanced.dict, { - autoRestart: Schema.boolean().description('应用在运行时崩溃自动重启。').default(true).hidden(), - timezoneOffset: Schema.number().description('时区偏移量 (分钟)。').default(new Date().getTimezoneOffset()), - stackTraceLimit: Schema.natural().description('报错的调用堆栈深度。').default(10), - plugins: Schema.object({}).hidden(), -}) - -export const name = 'CLI' - -Context.service('loader') - -export function prepare(config: App.Config) { - logger.prepare(config.logger) - - if (config.timezoneOffset !== undefined) { - Time.setTimezoneOffset(config.timezoneOffset) - } - - if (config.stackTraceLimit !== undefined) { - Error.stackTraceLimit = config.stackTraceLimit - } -} - -export function apply(ctx: Context, config: App.Config) { - logger.apply(ctx.app) - ctx.plugin(daemon, config) - - if (process.env.KOISHI_WATCH_ROOT !== undefined) { - (config.watch ??= {}).root = process.env.KOISHI_WATCH_ROOT - } - - if (config.watch) ctx.plugin(Watcher, config.watch) -} diff --git a/packages/cli/src/index.ts b/packages/cli/src/bin.ts similarity index 79% rename from packages/cli/src/index.ts rename to packages/cli/src/bin.ts index ef6b35710f..ea451cef11 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/bin.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node +import registerCreateCommand from './create' import registerStartCommand from './start' import CAC from 'cac' @@ -8,6 +9,7 @@ declare const KOISHI_VERSION: string const cli = CAC('koishi').help().version(KOISHI_VERSION) registerStartCommand(cli) +registerCreateCommand(cli) cli.parse() diff --git a/packages/cli/src/create.ts b/packages/cli/src/create.ts new file mode 100644 index 0000000000..4652146c9c --- /dev/null +++ b/packages/cli/src/create.ts @@ -0,0 +1,108 @@ +import { CAC } from 'cac' +import { promises as fsp } from 'fs' +import { resolve } from 'path' +import { getAgent } from './utils' +import spawn from 'cross-spawn' +import kleur from 'kleur' +import prompts from 'prompts' + +class Runner { + meta: any + root: string + name: string + fullname: string + + async init(name: string) { + this.name = name || await this.getName() + this.fullname = name.includes('/') + ? name.replace('/', '/koishi-plugin-') + : 'koishi-plugin-' + name + this.root = resolve(process.cwd(), 'plugins', name) + await this.write() + } + + async start(name: string) { + const [agent] = await Promise.all([ + getAgent(), + this.init(name), + ]) + const args: string[] = agent === 'yarn' ? [] : ['install'] + spawn.sync(agent, args, { stdio: 'inherit' }) + } + + async getName() { + const { name } = await prompts({ + type: 'text', + name: 'name', + message: 'plugin name:', + }) + return name.trim() as string + } + + async write() { + await fsp.mkdir(this.root + '/src', { recursive: true }) + await Promise.all([ + this.writeManifest(), + this.writeTsConfig(), + this.writeIndex(), + ]) + } + + async writeManifest() { + await fsp.writeFile(this.root + '/package.json', JSON.stringify({ + name: this.fullname, + version: '1.0.0', + main: 'lib/index.js', + typings: 'lib/index.d.ts', + files: ['lib'], + license: 'MIT', + scripts: { + build: 'tsc -b', + }, + keywords: [ + 'chatbot', + 'koishi', + 'plugin', + ], + peerDependencies: { + koishi: this.meta.dependencies.koishi, + }, + }, null, 2)) + } + + async writeTsConfig() { + await fsp.writeFile(this.root + '/tsconfig.json', JSON.stringify({ + extends: '../../tsconfig.base', + compilerOptions: { + rootDir: 'src', + outDir: 'lib', + }, + include: ['src'], + }, null, 2)) + } + + async writeIndex() { + await fsp.writeFile(this.root + '/src/index.ts', [ + `import { Context } from 'koishi'`, + '', + `export const name = '${this.name.replace(/^@\w+\//, '')}'`, + '', + `export function apply(ctx: Context) {`, + ` // write your plugin here`, + `}`, + '', + ].join('\n')) + } +} + +export default function (cli: CAC) { + cli.command('create [name]', 'create a plugin') + .action(async (name: string, options) => { + const meta = require(process.cwd() + '/package.json') + if (!meta.workspaces) { + console.log(kleur.red('error') + ' koishi create is only supported in workspaces.') + } else { + new Runner().start(name) + } + }) +} diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts new file mode 100644 index 0000000000..a53fdb8c89 --- /dev/null +++ b/packages/cli/src/utils.ts @@ -0,0 +1,39 @@ +import spawn from 'cross-spawn' +import { existsSync } from 'fs' +import { resolve } from 'path' + +export type { Loader, Watcher } from './worker' + +function supports(command: string, args: string[] = []) { + return new Promise((resolve) => { + const child = spawn(command, args, { stdio: 'ignore' }) + child.on('exit', (code) => { + resolve(!code) + }) + child.on('error', () => { + resolve(false) + }) + }) +} + +export type Agent = 'yarn' | 'npm' | 'pnpm' + +let agentTask: Promise + +async function $getAgent(cwd: string) { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { npm_execpath } = process.env + const isYarn = npm_execpath.includes('yarn') + if (isYarn) return 'yarn' + + if (existsSync(resolve(cwd, 'yarn.lock'))) return 'yarn' + if (existsSync(resolve(cwd, 'pnpm-lock.yaml'))) return 'pnpm' + if (existsSync(resolve(cwd, 'package-lock.json'))) return 'npm' + + const hasPnpm = await supports('pnpm', ['--version']) + return hasPnpm ? 'pnpm' : 'npm' +} + +export function getAgent(cwd = process.cwd()) { + return agentTask ||= $getAgent(cwd) +} diff --git a/packages/cli/src/worker.ts b/packages/cli/src/worker.ts deleted file mode 100644 index fa6bd659ec..0000000000 --- a/packages/cli/src/worker.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Logger } from 'koishi' -import { Loader } from './loader' -import * as addons from './addons' - -function handleException(error: any) { - new Logger('app').error(error) - process.exit(1) -} - -process.on('uncaughtException', handleException) - -process.on('unhandledRejection', (error) => { - new Logger('app').warn(error) -}) - -const loader = new Loader() -const config = loader.loadConfig() - -addons.prepare(config) - -const app = loader.createApp() - -app.plugin(addons, app.options) -app.start().then(() => { - for (const name in loader.cache) { - loader.diagnose(name) - } -}) diff --git a/packages/cli/src/addons/daemon.ts b/packages/cli/src/worker/daemon.ts similarity index 100% rename from packages/cli/src/addons/daemon.ts rename to packages/cli/src/worker/daemon.ts diff --git a/packages/cli/src/worker/index.ts b/packages/cli/src/worker/index.ts new file mode 100644 index 0000000000..995b9e9af3 --- /dev/null +++ b/packages/cli/src/worker/index.ts @@ -0,0 +1,80 @@ +import { App, Context, Dict, Logger, Schema, Time } from 'koishi' +import * as daemon from './daemon' +import * as logger from './logger' +import Loader from './loader' +import Watcher from './watcher' + +export { Loader, Watcher } + +declare module 'koishi' { + interface App { + prologue: string[] + watcher: Watcher + } + + namespace App { + interface Config extends daemon.Config { + plugins?: Dict + timezoneOffset?: number + stackTraceLimit?: number + logger?: logger.Config + watch?: Watcher.Config + } + } +} + +Object.assign(App.Config.Advanced.dict, { + autoRestart: Schema.boolean().description('应用在运行时崩溃自动重启。').default(true).hidden(), + timezoneOffset: Schema.number().description('时区偏移量 (分钟)。').default(new Date().getTimezoneOffset()), + stackTraceLimit: Schema.natural().description('报错的调用堆栈深度。').default(10), + plugins: Schema.object({}).hidden(), +}) + +function handleException(error: any) { + new Logger('app').error(error) + process.exit(1) +} + +process.on('uncaughtException', handleException) + +process.on('unhandledRejection', (error) => { + new Logger('app').warn(error) +}) + +const loader = new Loader() +const config = loader.loadConfig() + +logger.prepare(config.logger) + +if (config.timezoneOffset !== undefined) { + Time.setTimezoneOffset(config.timezoneOffset) +} + +if (config.stackTraceLimit !== undefined) { + Error.stackTraceLimit = config.stackTraceLimit +} + +const app = loader.createApp() + +namespace addons { + export const name = 'CLI' + + export function apply(ctx: Context, config: App.Config) { + logger.apply(ctx.app) + ctx.plugin(daemon, config) + + if (process.env.KOISHI_WATCH_ROOT !== undefined) { + (config.watch ??= {}).root = process.env.KOISHI_WATCH_ROOT + } + + if (config.watch) ctx.plugin(Watcher, config.watch) + } +} + +app.plugin(addons, app.options) + +app.start().then(() => { + for (const name in loader.cache) { + loader.diagnose(name) + } +}) diff --git a/packages/cli/src/loader.ts b/packages/cli/src/worker/loader.ts similarity index 96% rename from packages/cli/src/loader.ts rename to packages/cli/src/worker/loader.ts index a321ac47b2..f3a719e7cf 100644 --- a/packages/cli/src/loader.ts +++ b/packages/cli/src/worker/loader.ts @@ -1,6 +1,6 @@ import { dirname, extname, isAbsolute, resolve } from 'path' import { readdirSync, readFileSync, writeFileSync } from 'fs' -import { App, Dict, interpolate, Logger, Modules, unwrapExports, valueMap } from 'koishi' +import { App, Context, Dict, interpolate, Logger, Modules, unwrapExports, valueMap } from 'koishi' import * as yaml from 'js-yaml' import * as dotenv from 'dotenv' @@ -12,6 +12,8 @@ declare module 'koishi' { } } +Context.service('loader') + const oldPaths = Modules.internal.paths Modules.internal.paths = function (name: string) { // resolve absolute or relative path @@ -31,7 +33,7 @@ const context = { env: process.env, } -export class Loader { +export default class Loader { dirname: string filename: string extname: string diff --git a/packages/cli/src/addons/logger.ts b/packages/cli/src/worker/logger.ts similarity index 100% rename from packages/cli/src/addons/logger.ts rename to packages/cli/src/worker/logger.ts diff --git a/packages/cli/src/addons/watcher.ts b/packages/cli/src/worker/watcher.ts similarity index 100% rename from packages/cli/src/addons/watcher.ts rename to packages/cli/src/worker/watcher.ts diff --git a/packages/core/src/app.ts b/packages/core/src/app.ts index f7e666abb7..b998d5a2ee 100644 --- a/packages/core/src/app.ts +++ b/packages/core/src/app.ts @@ -303,6 +303,10 @@ export namespace App { Config.list.push(Config.Basic, Config.Features, Config.Advanced) } +export function defineConfig(config: App.Config) { + return config +} + class TaskQueue { #internal = new Set>() diff --git a/plugins/frontend/manager/src/installer.ts b/plugins/frontend/manager/src/installer.ts index 4b1eb27c4e..a03bc56163 100644 --- a/plugins/frontend/manager/src/installer.ts +++ b/plugins/frontend/manager/src/installer.ts @@ -1,7 +1,8 @@ import { clone, Context, Dict, Logger } from 'koishi' import { DataService } from '@koishijs/plugin-console' import { resolve } from 'path' -import { existsSync, promises as fsp } from 'fs' +import { promises as fsp } from 'fs' +import { getAgent } from '@koishijs/cli' import { Package } from './utils' import spawn from 'cross-spawn' @@ -12,24 +13,9 @@ declare module '@koishijs/plugin-console' { } } -type Agent = 'yarn' | 'npm' | 'pnpm' - const logger = new Logger('market') -function supports(command: string, args: string[] = []) { - return new Promise((resolve) => { - const child = spawn(command, args, { stdio: 'ignore' }) - child.on('exit', (code) => { - resolve(!code) - }) - child.on('error', () => { - resolve(false) - }) - }) -} - class Installer extends DataService> { - private agentTask: Promise private metaTask: Promise constructor(public ctx: Context) { @@ -43,24 +29,6 @@ class Installer extends DataService> { return this.ctx.app.baseDir } - async _getAgent(): Promise { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { npm_execpath } = process.env - const isYarn = npm_execpath.includes('yarn') - if (isYarn) return 'yarn' - - if (existsSync(resolve(this.cwd, 'yarn.lock'))) return 'yarn' - if (existsSync(resolve(this.cwd, 'pnpm-lock.yaml'))) return 'pnpm' - if (existsSync(resolve(this.cwd, 'package-lock.json'))) return 'npm' - - const hasPnpm = !isYarn && supports('pnpm', ['--version']) - return hasPnpm ? 'pnpm' : 'npm' - } - - getAgent() { - return this.agentTask ||= this._getAgent() - } - async _loadDeps() { const filename = resolve(this.cwd, 'package.json') const source = await fsp.readFile(filename, 'utf8') @@ -119,7 +87,7 @@ class Installer extends DataService> { installDep = async (deps: Dict) => { const [agent, meta] = await Promise.all([ - this.getAgent(), + getAgent(), this.override(deps), ]) const args: string[] = agent === 'yarn' ? [] : ['install']