From 2ae3d59de247ef20e25fcfc6b33cd4a2397ecd3c Mon Sep 17 00:00:00 2001 From: skdhg <46562212+skdhg@users.noreply.github.com> Date: Fri, 24 Nov 2023 20:50:06 +0545 Subject: [PATCH 1/5] refactor: improve cli --- .prettierignore | 7 +- package.json | 33 ++-- packages/commandkit/bin/build.mjs | 41 ++++- packages/commandkit/bin/common.mjs | 34 +++- packages/commandkit/bin/dev-server.mjs | 91 ----------- packages/commandkit/bin/development.mjs | 151 ++++++++++++++++++ packages/commandkit/bin/index.mjs | 16 +- packages/commandkit/bin/parse-env.mjs | 52 ++++++ packages/commandkit/bin/production.mjs | 72 +++++++++ packages/commandkit/package.json | 111 ++++++------- packages/commandkit/src/config.ts | 78 +++++++++ packages/commandkit/src/index.ts | 1 + packages/commandkit/tests/commandkit.json | 4 - packages/commandkit/tests/commandkit.mjs | 8 + .../tests/{ => src}/commands/misc/count.ts | 0 .../tests/{ => src}/commands/misc/giveaway.ts | 0 .../tests/{ => src}/commands/misc/ping.ts | 0 .../tests/{ => src}/commands/misc/reload.ts | 2 +- .../{ => src}/events/messageCreate/say-hi.ts | 0 .../{ => src}/events/ready/console-log.ts | 2 +- packages/commandkit/tests/{ => src}/index.ts | 7 +- .../tests/{ => src}/validations/devOnly.ts | 2 +- 22 files changed, 522 insertions(+), 190 deletions(-) delete mode 100644 packages/commandkit/bin/dev-server.mjs create mode 100644 packages/commandkit/bin/development.mjs create mode 100644 packages/commandkit/bin/parse-env.mjs create mode 100644 packages/commandkit/bin/production.mjs create mode 100644 packages/commandkit/src/config.ts delete mode 100644 packages/commandkit/tests/commandkit.json create mode 100644 packages/commandkit/tests/commandkit.mjs rename packages/commandkit/tests/{ => src}/commands/misc/count.ts (100%) rename packages/commandkit/tests/{ => src}/commands/misc/giveaway.ts (100%) rename packages/commandkit/tests/{ => src}/commands/misc/ping.ts (100%) rename packages/commandkit/tests/{ => src}/commands/misc/reload.ts (96%) rename packages/commandkit/tests/{ => src}/events/messageCreate/say-hi.ts (100%) rename packages/commandkit/tests/{ => src}/events/ready/console-log.ts (76%) rename packages/commandkit/tests/{ => src}/index.ts (76%) rename packages/commandkit/tests/{ => src}/validations/devOnly.ts (76%) diff --git a/.prettierignore b/.prettierignore index b052cdb..a93ce0c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,8 +1,9 @@ node_modules **/docs/.next/ -dist .astro +dist +.commandkit .DS_Store .vscode @@ -10,10 +11,12 @@ dist README.md pnpm-lock.yaml +package.json + npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* .env -.env.production \ No newline at end of file +.env.production diff --git a/package.json b/package.json index bd90d96..e09bce4 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,19 @@ { - "name": "commandkit", - "version": "0.0.0", - "private": true, - "license": "MIT", - "scripts": { - "dev": "turbo dev", - "lint": "turbo lint && prettier ./ --check --ignore-path=.prettierignore", - "build": "turbo lint && turbo build", - "build:package": "turbo lint --filter='commandkit' && turbo build --filter='commandkit'", - "deploy:package": "turbo lint --filter='commandkit' && turbo build --filter='commandkit' && turbo deploy --filter='commandkit'", - "deploy:package-dev": "turbo lint --filter='commandkit' && turbo build --filter='commandkit' && turbo deploy-dev --filter='commandkit'" - }, - "devDependencies": { - "prettier": "^3.0.3", - "turbo": "^1.10.13" - } + "name": "commandkit", + "version": "0.0.0", + "private": true, + "license": "MIT", + "scripts": { + "dev": "turbo dev", + "lint": "turbo lint && prettier ./ --check --ignore-path=.prettierignore", + "build": "turbo lint && turbo build", + "build:package": "turbo lint --filter='commandkit' && turbo build --filter='commandkit'", + "deploy:package": "turbo lint --filter='commandkit' && turbo build --filter='commandkit' && turbo deploy --filter='commandkit'", + "deploy:package-dev": "turbo lint --filter='commandkit' && turbo build --filter='commandkit' && turbo deploy-dev --filter='commandkit'", + "format": "prettier --write \"./{packages,apps}/**/*.{js,ts,jsx,tsx,mjs,mts,cjs,cts,css,md,mdx}\"" + }, + "devDependencies": { + "prettier": "^3.0.3", + "turbo": "^1.10.13" + } } diff --git a/packages/commandkit/bin/build.mjs b/packages/commandkit/bin/build.mjs index 1bd4883..db7a23b 100644 --- a/packages/commandkit/bin/build.mjs +++ b/packages/commandkit/bin/build.mjs @@ -1,11 +1,13 @@ // @ts-check import { build } from 'tsup'; -import { Colors, erase, findCommandKitJSON, panic, write } from './common.mjs'; +import { Colors, erase, findCommandKitConfig, panic, write } from './common.mjs'; import ora from 'ora'; +import { appendFile } from 'node:fs/promises'; +import { join } from 'node:path'; export async function bootstrapProductionBuild(config) { - const { minify = false, outDir = 'dist', main, src } = findCommandKitJSON(config); + const { sourcemap = false, minify = false, outDir = 'dist', antiCrash = true, src, main } = await findCommandKitConfig(config); const status = ora('Creating optimized production build...\n').start(); const start = performance.now(); @@ -21,21 +23,23 @@ export async function bootstrapProductionBuild(config) { minify, shims: true, banner: { - js: '/* Optimized production build of your project, generated by CommandKit */', + js: '/* Optimized production build generated by CommandKit */', }, - sourcemap: false, + sourcemap, keepNames: true, outDir, silent: true, - entry: [src, '!dist', '!.commandkit'], + entry: [src, '!dist', '!.commandkit', `!${outDir}`], }); + if (antiCrash) await injectAntiCrash(outDir, main); + status.succeed( Colors.green(`Build completed in ${(performance.now() - start).toFixed(2)}ms!`), ); write( Colors.green( - `\nRun ${Colors.magenta(`node ${outDir}/${main}`)} ${Colors.green( + `\nRun ${Colors.magenta(`commandkit start`)} ${Colors.green( 'to start your bot.', )}`, ), @@ -45,3 +49,28 @@ export async function bootstrapProductionBuild(config) { panic(e); } } + +function injectAntiCrash(outDir, main) { + const path = join(process.cwd(), outDir, main); + const snippet = [ + "\n\n// --- CommandKit Anti-Crash Monitor ---", + ";(()=>{", + " 'use strict';", + " // 'uncaughtException' event is supposed to be used to perform synchronous cleanup before shutting down the process", + " // instead of using it as a means to resume operation.", + " // But it exists here due to compatibility reasons with discord bot ecosystem.", + " const p = (t) => `\\x1b[33m${t}\\x1b[0m`, b = '[CommandKit Anti-Crash Monitor]', l = console.log, e1 = 'uncaughtException', e2 = 'unhandledRejection';", + " if (!process.eventNames().includes(e1)) // skip if it is already handled", + " process.on(e1, (e, o) => {", + " l(p(`${b} Uncaught Exception`)); l(p(b), e.stack || e); l(p(`${b} Exception origin: ${o}`));", + " })", + " if (!process.eventNames().includes(e2)) // skip if it is already handled", + " on(e2, (r) => {", + " l(p(`${b} Unhandled Rejection at promise`)); l(p(`${b} ${r.stack || r}`));", + " });", + "})();", + "// --- CommandKit Anti-Crash Monitor ---\n", + ].join('\n'); + + return appendFile(path, snippet); +} \ No newline at end of file diff --git a/packages/commandkit/bin/common.mjs b/packages/commandkit/bin/common.mjs index 4a076b2..4fd671c 100644 --- a/packages/commandkit/bin/common.mjs +++ b/packages/commandkit/bin/common.mjs @@ -58,15 +58,39 @@ export function findPackageJSON() { return JSON.parse(fs.readFileSync(target, 'utf8')); } -export function findCommandKitJSON(src) { +const possibleFileNames = [ + 'commandkit.json', 'commandkit.config.json', + 'commandkit.js', 'commandkit.config.js', + 'commandkit.mjs', 'commandkit.config.mjs', + 'commandkit.cjs', 'commandkit.config.cjs' +]; + +export async function findCommandKitConfig(src) { const cwd = process.cwd(); - const target = src || join(cwd, 'commandkit.json'); + const locations = src ? [join(cwd, src)] : possibleFileNames.map(name => join(cwd, name)); - if (!fs.existsSync(target)) { - panic('Could not find commandkit.json in current directory.'); + for (const location of locations) { + try { + return await loadConfigInner(location); + } catch (e) { + continue; + } } - return JSON.parse(fs.readFileSync(target, 'utf8')); + panic('Could not locate commandkit config.'); +} + +async function loadConfigInner(target) { + const isJSON = target.endsWith('.json'); + + /** + * @type {import('..').CommandKitConfig} + */ + const config = await import(`file://${target}`, { + assert: isJSON ? { type: 'json' } : undefined, + }).then(conf => conf.default || conf); + + return config; } export function erase(dir) { diff --git a/packages/commandkit/bin/dev-server.mjs b/packages/commandkit/bin/dev-server.mjs deleted file mode 100644 index bf536bb..0000000 --- a/packages/commandkit/bin/dev-server.mjs +++ /dev/null @@ -1,91 +0,0 @@ -// @ts-check -import { config as dotenv } from 'dotenv'; -import { build } from 'tsup'; -import child_process from 'node:child_process'; -import ora from 'ora'; -import { join } from 'node:path'; -import { Colors, erase, findCommandKitJSON, panic, write } from './common.mjs'; - -export async function bootstrapDevelopmentServer(config) { - const { src, main = 'index.mjs', nodeOptions = ['--watch'] } = findCommandKitJSON(config); - - if (!src) { - panic('Could not find src in commandkit.json'); - } - - const status = ora(Colors.green('Starting a development server...\n')).start(); - const start = performance.now(); - - erase('.commandkit'); - - try { - await build({ - clean: true, - format: ['esm'], - dts: false, - skipNodeModulesBundle: true, - minify: false, - shims: true, - sourcemap: false, - keepNames: true, - outDir: '.commandkit', - silent: true, - entry: [src, '!dist', '!.commandkit'], - watch: nodeOptions.includes('--watch'), - }); - - status.succeed( - Colors.green(`Server started in ${(performance.now() - start).toFixed(2)}ms!\n`), - ); - - const processEnv = {}; - - const env = dotenv({ - path: join(process.cwd(), '.env'), - // @ts-expect-error - processEnv, - }); - - if (env.error) { - write(Colors.yellow(`[DOTENV] Warning: ${env.error.message}`)); - } - - if (env.parsed) { - write(Colors.blue('[DOTENV] Loaded .env file!')); - } - - const ps = child_process.spawn( - 'node', - [...nodeOptions, join(process.cwd(), '.commandkit', main)], - { - env: { - ...process.env, - ...processEnv, - NODE_ENV: 'development', - COMMANDKIT_DEV: 'true', - }, - cwd: process.cwd(), - }, - ); - - ps.stdout.on('data', (data) => { - write(data.toString()); - }); - - ps.stderr.on('data', (data) => { - write(Colors.red(data.toString())); - }); - - ps.on('close', (code) => { - write('\n'); - process.exit(code ?? 0); - }); - - ps.on('error', (err) => { - panic(err); - }); - } catch (e) { - status.fail(`Error occurred after ${(performance.now() - start).toFixed(2)}ms!\n`); - panic(e); - } -} diff --git a/packages/commandkit/bin/development.mjs b/packages/commandkit/bin/development.mjs new file mode 100644 index 0000000..4f13896 --- /dev/null +++ b/packages/commandkit/bin/development.mjs @@ -0,0 +1,151 @@ +// @ts-check +import { config as dotenv } from 'dotenv'; +import { build } from 'tsup'; +import child_process from 'node:child_process'; +import ora from 'ora'; +import { join } from 'node:path'; +import { Colors, erase, findCommandKitConfig, panic, write } from './common.mjs'; +import { parseEnv } from './parse-env.mjs'; + +const RESTARTING_MSG_PATTERN = /^Restarting '|".+'|"\n?$/; +const FAILED_RUNNING_PATTERN = /^Failed running '.+'|"\n?$/; + +export async function bootstrapDevelopmentServer(opts) { + const { + src, + main, + watch = Boolean(opts.noWatch), + nodeOptions = [], + envExtra = true, + clearRestartLogs = true, + outDir + } = await findCommandKitConfig(opts.config); + + if (!src) { + panic('Could not find src in commandkit.json'); + } + + if (!main) { + panic('Could not find main in commandkit.json'); + } + + const watchMode = watch; + const status = ora(Colors.green('Starting a development server...\n')).start(); + const start = performance.now(); + + if (watchMode && !nodeOptions.includes('--watch')) { + nodeOptions.push('--watch'); + } else if (!watchMode && nodeOptions.includes('--watch')) { + nodeOptions.splice(nodeOptions.indexOf('--watch'), 1); + } + + if (!nodeOptions.includes('--enable-source-maps')) { + nodeOptions.push('--enable-source-maps'); + } + + erase('.commandkit'); + + try { + await build({ + clean: true, + format: ['esm'], + dts: false, + skipNodeModulesBundle: true, + minify: false, + shims: true, + sourcemap: 'inline', + keepNames: true, + outDir: '.commandkit', + silent: true, + entry: [src, '!dist', '!.commandkit', `!${outDir}`].filter(Boolean), + watch: watchMode, + }); + + status.succeed( + Colors.green(`Dev server started in ${(performance.now() - start).toFixed(2)}ms!\n`), + ); + + if (watchMode) write(Colors.cyan('Watching for file changes...\n')); + + const processEnv = {}; + + const env = dotenv({ + path: join(process.cwd(), '.env'), + // @ts-expect-error + processEnv, + }); + + if (envExtra) { + parseEnv(processEnv); + } + + if (env.error) { + write(Colors.yellow(`[DOTENV] Warning: ${env.error.message}`)); + } + + if (env.parsed) { + write(Colors.blue('[DOTENV] Loaded .env file!')); + } + + /** + * @type {child_process.ChildProcessWithoutNullStreams} + */ + const ps = child_process.spawn( + 'node', + [...nodeOptions, join(process.cwd(), '.commandkit', main)], + { + env: { + ...process.env, + ...processEnv, + NODE_ENV: 'development', + // @ts-expect-error + COMMANDKIT_DEV: true, + // @ts-expect-error + COMMANDKIT_PRODUCTION: false, + }, + cwd: process.cwd(), + }, + ); + + let isLastLogRestarting = false; + + ps.stdout.on('data', (data) => { + const message = data.toString(); + + if (FAILED_RUNNING_PATTERN.test(message)) { + write(Colors.cyan('Failed running the bot, waiting for changes...')); + isLastLogRestarting = false; + return; + } + + if (clearRestartLogs && !RESTARTING_MSG_PATTERN.test(message)) { + write(message); + isLastLogRestarting = false; + } else { + if (isLastLogRestarting) return; + write(Colors.cyan('⌀ Restarting the bot...')); + isLastLogRestarting = true; + } + }); + + ps.stderr.on('data', (data) => { + const message = data.toString(); + + if (message.includes('ExperimentalWarning: Watch mode is an experimental feature and might change at any time')) return; + + write(Colors.red(message)); + }); + + ps.on('close', (code) => { + write('\n'); + process.exit(code ?? 0); + }); + + ps.on('error', (err) => { + panic(err); + }); + } catch (e) { + status.fail(`Error occurred after ${(performance.now() - start).toFixed(2)}ms!\n`); + panic(e.stack ?? e); + } +} diff --git a/packages/commandkit/bin/index.mjs b/packages/commandkit/bin/index.mjs index ff6cb13..b92556a 100644 --- a/packages/commandkit/bin/index.mjs +++ b/packages/commandkit/bin/index.mjs @@ -3,18 +3,28 @@ // @ts-check import { Command } from 'commander'; -import { bootstrapDevelopmentServer } from './dev-server.mjs'; +import { bootstrapDevelopmentServer } from './development.mjs'; import { bootstrapProductionBuild } from './build.mjs'; +import { bootstrapProductionServer } from './production.mjs'; const program = new Command('commandkit'); program .command('dev') .description('Start your bot in development mode.') - .option('-c, --config ', 'Path to your commandkit.json file.', './commandkit.json') + .option('-c, --config ', 'Path to your commandkit config file.', './commandkit.js') + .action(() => { + const options = program.opts(); + bootstrapDevelopmentServer(options); + }); + +program + .command('start') + .description('Start your bot in production mode after running the build command.') + .option('-c, --config ', 'Path to your commandkit.json file.', './commandkit.js') .action(() => { const options = program.opts(); - bootstrapDevelopmentServer(options.config); + bootstrapProductionServer(options.config); }); program diff --git a/packages/commandkit/bin/parse-env.mjs b/packages/commandkit/bin/parse-env.mjs new file mode 100644 index 0000000..abe5add --- /dev/null +++ b/packages/commandkit/bin/parse-env.mjs @@ -0,0 +1,52 @@ +// @ts-check + +import { randomUUID } from 'node:crypto'; + +const valuesMap = { + true: true, + false: false, + null: null, + undefined: undefined, + __UUIDv4__: () => randomUUID(), +}; + +const VALUE_PREFIXES = { + JSON: 'JSON::', + DATE: 'DATE::', +}; + +export function parseEnv(src) { + for (const key in src) { + const value = src[key]; + + if (typeof value !== 'string') continue; + + if (value.startsWith(VALUE_PREFIXES.JSON)) { + src[key] = JSON.parse(value.replace(VALUE_PREFIXES.JSON, '')); + continue; + } + + if (value.startsWith(VALUE_PREFIXES.DATE)) { + src[key] = new Date(value.replace(VALUE_PREFIXES.DATE, '')); + continue; + } + + if (value.includes(',')) { + src[key] = value.split(',').map((v) => v.trim()); + continue; + } + + if (/^[0-9]+n$/.test(value)) { + src[key] = BigInt(value); + continue; + } + + if (value in valuesMap) { + src[key] = + typeof valuesMap[value] === 'function' ? valuesMap[value]() : valuesMap[value]; + continue; + } + } + + return src; +} diff --git a/packages/commandkit/bin/production.mjs b/packages/commandkit/bin/production.mjs new file mode 100644 index 0000000..3bb0a4c --- /dev/null +++ b/packages/commandkit/bin/production.mjs @@ -0,0 +1,72 @@ +// @ts-check +import { config as dotenv } from 'dotenv'; +import child_process from 'node:child_process'; +import { join } from 'node:path'; +import { Colors, findCommandKitConfig, panic, write } from './common.mjs'; +import { existsSync } from 'node:fs'; +import { parseEnv } from './parse-env.mjs'; + +export async function bootstrapProductionServer(config) { + const { main, outDir = 'dist', envExtra = true } = await findCommandKitConfig(config); + + if (!existsSync(join(process.cwd(), outDir, main))) { + panic('Could not find production build, maybe run `commandkit build` first?'); + } + + try { + const processEnv = {}; + + const env = dotenv({ + path: join(process.cwd(), '.env'), + // @ts-expect-error + processEnv, + }); + + if (envExtra) { + parseEnv(processEnv); + } + + if (env.error) { + write(Colors.yellow(`[DOTENV] Warning: ${env.error.message}`)); + } + + if (env.parsed) { + write(Colors.blue('[DOTENV] Loaded .env file!')); + } + + /** + * @type {child_process.ChildProcessWithoutNullStreams} + */ + const ps = child_process.spawn('node', [join(process.cwd(), '.commandkit', main)], { + env: { + ...process.env, + ...processEnv, + NODE_ENV: 'production', + // @ts-expect-error + COMMANDKIT_DEV: false, + // @ts-expect-error + COMMANDKIT_PROD: true, + }, + cwd: process.cwd(), + }); + + ps.stdout.on('data', (data) => { + write(data.toString()); + }); + + ps.stderr.on('data', (data) => { + write(Colors.red(data.toString())); + }); + + ps.on('close', (code) => { + write('\n'); + process.exit(code ?? 0); + }); + + ps.on('error', (err) => { + panic(err); + }); + } catch (e) { + panic(e); + } +} diff --git a/packages/commandkit/package.json b/packages/commandkit/package.json index 304d279..16b37e3 100644 --- a/packages/commandkit/package.json +++ b/packages/commandkit/package.json @@ -1,57 +1,58 @@ { - "name": "commandkit", - "description": "Beginner friendly command & event handler for Discord.js", - "version": "0.1.6", - "license": "MIT", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "bin": "./bin/index.mjs", - "exports": { - ".": { - "require": "./dist/index.js", - "import": "./dist/index.mjs", - "types": "./dist/index.d.ts" - } - }, - "files": [ - "dist", - "bin" - ], - "scripts": { - "lint": "tsc", - "dev": "tsup --watch", - "build": "tsup", - "deploy": "npm publish", - "deploy-dev": "npm publish --access public --tag dev", - "test": "tsx tests/index.ts", - "test:watch": "tsx watch tests/index.ts" - }, - "repository": { - "url": "git+https://github.com/underctrl-io/commandkit.git" - }, - "homepage": "https://commandkit.js.org", - "keywords": [ - "discord.js", - "command handler", - "event handler" - ], - "dependencies": { - "commander": "^11.1.0", - "ora": "^7.0.1", - "rfdc": "^1.3.0", - "rimraf": "^5.0.5", - "tsup": "^7.2.0" - }, - "devDependencies": { - "@types/node": "^20.5.9", - "discord.js": "^14.13.0", - "dotenv": "^16.3.1", - "tsconfig": "workspace:*", - "tsx": "^3.12.8", - "typescript": "^5.1.6" - }, - "peerDependencies": { - "discord.js": "^14" + "name": "commandkit", + "description": "Beginner friendly command & event handler for Discord.js", + "version": "0.1.6", + "license": "MIT", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "bin": "./bin/index.mjs", + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/index.mjs", + "types": "./dist/index.d.ts" } -} + }, + "files": [ + "dist", + "bin" + ], + "scripts": { + "lint": "tsc", + "dev": "tsup --watch", + "build": "tsup", + "deploy": "npm publish", + "deploy-dev": "npm publish --access public --tag dev", + "test": "cd ./tests && node ../bin/index.mjs dev", + "test:build": "cd ./tests && node ../bin/index.mjs build", + "test:prod": "cd ./tests && node ../bin/index.mjs start" + }, + "repository": { + "url": "git+https://github.com/underctrl-io/commandkit.git" + }, + "homepage": "https://commandkit.js.org", + "keywords": [ + "discord.js", + "command handler", + "event handler" + ], + "dependencies": { + "commander": "^11.1.0", + "ora": "^7.0.1", + "rfdc": "^1.3.0", + "rimraf": "^5.0.5", + "tsup": "^7.2.0" + }, + "devDependencies": { + "@types/node": "^20.5.9", + "discord.js": "^14.13.0", + "dotenv": "^16.3.1", + "tsconfig": "workspace:*", + "tsx": "^3.12.8", + "typescript": "^5.1.6" + }, + "peerDependencies": { + "discord.js": "^14" + } +} \ No newline at end of file diff --git a/packages/commandkit/src/config.ts b/packages/commandkit/src/config.ts new file mode 100644 index 0000000..ce96235 --- /dev/null +++ b/packages/commandkit/src/config.ts @@ -0,0 +1,78 @@ +export interface CommandKitConfig { + /** + * The source directory of the project. + */ + src: string; + /** + * The main "javascript" file of the project. + */ + main: string; + /** + * Whether or not to use the watch mode. Defaults to `true`. + */ + watch: boolean; + /** + * The output directory of the project. Defaults to `dist`. + */ + outDir: string; + /** + * Whether or not to include extra env utilities. Defaults to `true`. + */ + envExtra: boolean; + /** + * Node.js cli options. + */ + nodeOptions: string[]; + /** + * Whether or not to clear default restart logs. Defaults to `true`. + */ + clearRestartLogs: boolean; + /** + * Whether or not to minify the output. Defaults to `false`. + */ + minify: boolean; + /** + * Whether or not to include sourcemaps in production build. Defaults to `false`. + */ + sourcemap: boolean | 'inline'; + /** + * Whether or not to include anti-crash handler in production. Defaults to `true`. + */ + antiCrash: boolean; +} + +let globalConfig: Partial = { + envExtra: true, + outDir: 'dist', + watch: true, + clearRestartLogs: true, + minify: false, + sourcemap: false, + nodeOptions: [], + antiCrash: true, +}; + +export function getConfig(): CommandKitConfig { + return globalConfig as CommandKitConfig; +} + +const requiredProps = ['src', 'main'] as const; + +type R = (typeof requiredProps)[number]; + +type PartialConfig = Partial> & Pick; + +export function defineConfig(config: PartialConfig) { + for (const prop of requiredProps) { + if (!config[prop]) { + throw new Error(`[CommandKit Config] Missing required config property: ${prop}`); + } + } + + globalConfig = { + ...globalConfig, + ...config, + }; + + return globalConfig; +} diff --git a/packages/commandkit/src/index.ts b/packages/commandkit/src/index.ts index e7d69b5..704f164 100644 --- a/packages/commandkit/src/index.ts +++ b/packages/commandkit/src/index.ts @@ -1,4 +1,5 @@ export * from './CommandKit'; export * from './components'; +export * from './config'; export * from './utils/signal'; export * from './types'; diff --git a/packages/commandkit/tests/commandkit.json b/packages/commandkit/tests/commandkit.json deleted file mode 100644 index 7ae65ca..0000000 --- a/packages/commandkit/tests/commandkit.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "src": ".", - "main": "index.mjs" -} diff --git a/packages/commandkit/tests/commandkit.mjs b/packages/commandkit/tests/commandkit.mjs new file mode 100644 index 0000000..1b0a280 --- /dev/null +++ b/packages/commandkit/tests/commandkit.mjs @@ -0,0 +1,8 @@ +// @ts-check + +import { defineConfig } from '../dist/index.mjs'; + +export default defineConfig({ + main: 'index.mjs', + src: 'src', +}); \ No newline at end of file diff --git a/packages/commandkit/tests/commands/misc/count.ts b/packages/commandkit/tests/src/commands/misc/count.ts similarity index 100% rename from packages/commandkit/tests/commands/misc/count.ts rename to packages/commandkit/tests/src/commands/misc/count.ts diff --git a/packages/commandkit/tests/commands/misc/giveaway.ts b/packages/commandkit/tests/src/commands/misc/giveaway.ts similarity index 100% rename from packages/commandkit/tests/commands/misc/giveaway.ts rename to packages/commandkit/tests/src/commands/misc/giveaway.ts diff --git a/packages/commandkit/tests/commands/misc/ping.ts b/packages/commandkit/tests/src/commands/misc/ping.ts similarity index 100% rename from packages/commandkit/tests/commands/misc/ping.ts rename to packages/commandkit/tests/src/commands/misc/ping.ts diff --git a/packages/commandkit/tests/commands/misc/reload.ts b/packages/commandkit/tests/src/commands/misc/reload.ts similarity index 96% rename from packages/commandkit/tests/commands/misc/reload.ts rename to packages/commandkit/tests/src/commands/misc/reload.ts index ea45afd..19ecd2c 100644 --- a/packages/commandkit/tests/commands/misc/reload.ts +++ b/packages/commandkit/tests/src/commands/misc/reload.ts @@ -1,4 +1,4 @@ -import { SlashCommandProps, CommandOptions, CommandData } from '../../../src/index'; +import { SlashCommandProps, CommandOptions, CommandData } from '../../../../src/index'; export const data: CommandData = { name: 'reload', diff --git a/packages/commandkit/tests/events/messageCreate/say-hi.ts b/packages/commandkit/tests/src/events/messageCreate/say-hi.ts similarity index 100% rename from packages/commandkit/tests/events/messageCreate/say-hi.ts rename to packages/commandkit/tests/src/events/messageCreate/say-hi.ts diff --git a/packages/commandkit/tests/events/ready/console-log.ts b/packages/commandkit/tests/src/events/ready/console-log.ts similarity index 76% rename from packages/commandkit/tests/events/ready/console-log.ts rename to packages/commandkit/tests/src/events/ready/console-log.ts index 0042a41..dd20cd2 100644 --- a/packages/commandkit/tests/events/ready/console-log.ts +++ b/packages/commandkit/tests/src/events/ready/console-log.ts @@ -1,5 +1,5 @@ import type { Client } from 'discord.js'; -import type { CommandKit } from '../../../src'; +import type { CommandKit } from '../../../../src'; export default function (c: Client, client, handler: CommandKit) { console.log(`${c.user.username} is online.`); diff --git a/packages/commandkit/tests/index.ts b/packages/commandkit/tests/src/index.ts similarity index 76% rename from packages/commandkit/tests/index.ts rename to packages/commandkit/tests/src/index.ts index 657d8f5..3d35a48 100644 --- a/packages/commandkit/tests/index.ts +++ b/packages/commandkit/tests/src/index.ts @@ -1,8 +1,5 @@ -import { CommandKit } from '../src'; +import { CommandKit } from '../../src/index'; import { Client } from 'discord.js'; -import { config } from 'dotenv'; - -config({ path: `${__dirname}/.env` }); const client = new Client({ intents: ['Guilds', 'GuildMembers', 'GuildMessages', 'MessageContent'], @@ -18,4 +15,4 @@ new CommandKit({ bulkRegister: true, }); -client.login(process.env.TOKEN); +await client.login(process.env.TOKEN); diff --git a/packages/commandkit/tests/validations/devOnly.ts b/packages/commandkit/tests/src/validations/devOnly.ts similarity index 76% rename from packages/commandkit/tests/validations/devOnly.ts rename to packages/commandkit/tests/src/validations/devOnly.ts index 993c41e..30c6207 100644 --- a/packages/commandkit/tests/validations/devOnly.ts +++ b/packages/commandkit/tests/src/validations/devOnly.ts @@ -1,4 +1,4 @@ -import type { ValidationFunctionProps } from '../../src'; +import type { ValidationFunctionProps } from '../../../src'; export default function ({ interaction, commandObj, handler }: ValidationFunctionProps) { if (commandObj.data.name === 'ping') { From dd3db80e345242eccc6d97dd706a24b398a6054f Mon Sep 17 00:00:00 2001 From: skdhg <46562212+skdhg@users.noreply.github.com> Date: Fri, 24 Nov 2023 20:55:30 +0545 Subject: [PATCH 2/5] fix: improve logs --- packages/commandkit/bin/development.mjs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/commandkit/bin/development.mjs b/packages/commandkit/bin/development.mjs index 4f13896..7097832 100644 --- a/packages/commandkit/bin/development.mjs +++ b/packages/commandkit/bin/development.mjs @@ -107,7 +107,7 @@ export async function bootstrapDevelopmentServer(opts) { }, ); - let isLastLogRestarting = false; + let isLastLogRestarting = false, hasStarted = false; ps.stdout.on('data', (data) => { const message = data.toString(); @@ -115,6 +115,7 @@ export async function bootstrapDevelopmentServer(opts) { if (FAILED_RUNNING_PATTERN.test(message)) { write(Colors.cyan('Failed running the bot, waiting for changes...')); isLastLogRestarting = false; + if (!hasStarted) hasStarted = true; return; } @@ -122,10 +123,15 @@ export async function bootstrapDevelopmentServer(opts) { write(message); isLastLogRestarting = false; } else { - if (isLastLogRestarting) return; + if (isLastLogRestarting || !hasStarted) { + if (!hasStarted) hasStarted = true; + return; + } write(Colors.cyan('⌀ Restarting the bot...')); isLastLogRestarting = true; } + + if (!hasStarted) hasStarted = true; }); ps.stderr.on('data', (data) => { From 24e848e6e653e68e22d963466893ddfda958f4bb Mon Sep 17 00:00:00 2001 From: skdhg <46562212+skdhg@users.noreply.github.com> Date: Fri, 24 Nov 2023 21:04:31 +0545 Subject: [PATCH 3/5] fix: sourcemap now works correctly --- packages/commandkit/bin/build.mjs | 2 +- packages/commandkit/bin/production.mjs | 4 ++-- packages/commandkit/tests/src/index.ts | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/commandkit/bin/build.mjs b/packages/commandkit/bin/build.mjs index db7a23b..15eef40 100644 --- a/packages/commandkit/bin/build.mjs +++ b/packages/commandkit/bin/build.mjs @@ -65,7 +65,7 @@ function injectAntiCrash(outDir, main) { " l(p(`${b} Uncaught Exception`)); l(p(b), e.stack || e); l(p(`${b} Exception origin: ${o}`));", " })", " if (!process.eventNames().includes(e2)) // skip if it is already handled", - " on(e2, (r) => {", + " process.on(e2, (r) => {", " l(p(`${b} Unhandled Rejection at promise`)); l(p(`${b} ${r.stack || r}`));", " });", "})();", diff --git a/packages/commandkit/bin/production.mjs b/packages/commandkit/bin/production.mjs index 3bb0a4c..82d0680 100644 --- a/packages/commandkit/bin/production.mjs +++ b/packages/commandkit/bin/production.mjs @@ -7,7 +7,7 @@ import { existsSync } from 'node:fs'; import { parseEnv } from './parse-env.mjs'; export async function bootstrapProductionServer(config) { - const { main, outDir = 'dist', envExtra = true } = await findCommandKitConfig(config); + const { main, outDir = 'dist', envExtra = true, sourcemap } = await findCommandKitConfig(config); if (!existsSync(join(process.cwd(), outDir, main))) { panic('Could not find production build, maybe run `commandkit build` first?'); @@ -37,7 +37,7 @@ export async function bootstrapProductionServer(config) { /** * @type {child_process.ChildProcessWithoutNullStreams} */ - const ps = child_process.spawn('node', [join(process.cwd(), '.commandkit', main)], { + const ps = child_process.spawn('node', [sourcemap ? '--enable-source-maps' : '', join(process.cwd(), outDir, main)].filter(Boolean), { env: { ...process.env, ...processEnv, diff --git a/packages/commandkit/tests/src/index.ts b/packages/commandkit/tests/src/index.ts index 3d35a48..a4a6945 100644 --- a/packages/commandkit/tests/src/index.ts +++ b/packages/commandkit/tests/src/index.ts @@ -15,4 +15,7 @@ new CommandKit({ bulkRegister: true, }); +console.log('io23hfio3h3iohgio3'); +throw new Error('oh no'); + await client.login(process.env.TOKEN); From ce1a499acbde00a5f941f24f32f67f8fda1c571f Mon Sep 17 00:00:00 2001 From: skdhg <46562212+skdhg@users.noreply.github.com> Date: Fri, 24 Nov 2023 21:12:21 +0545 Subject: [PATCH 4/5] fix: update anticrash injection --- packages/commandkit/bin/build.mjs | 4 ++-- packages/commandkit/tests/src/index.ts | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/commandkit/bin/build.mjs b/packages/commandkit/bin/build.mjs index 15eef40..4b38bfe 100644 --- a/packages/commandkit/bin/build.mjs +++ b/packages/commandkit/bin/build.mjs @@ -62,11 +62,11 @@ function injectAntiCrash(outDir, main) { " const p = (t) => `\\x1b[33m${t}\\x1b[0m`, b = '[CommandKit Anti-Crash Monitor]', l = console.log, e1 = 'uncaughtException', e2 = 'unhandledRejection';", " if (!process.eventNames().includes(e1)) // skip if it is already handled", " process.on(e1, (e, o) => {", - " l(p(`${b} Uncaught Exception`)); l(p(b), e.stack || e); l(p(`${b} Exception origin: ${o}`));", + " l(p(`${b} Uncaught Exception`)); l(p(b), p(e.stack || e));", " })", " if (!process.eventNames().includes(e2)) // skip if it is already handled", " process.on(e2, (r) => {", - " l(p(`${b} Unhandled Rejection at promise`)); l(p(`${b} ${r.stack || r}`));", + " l(p(`${b} Unhandled promise rejection`)); l(p(`${b} ${r.stack || r}`));", " });", "})();", "// --- CommandKit Anti-Crash Monitor ---\n", diff --git a/packages/commandkit/tests/src/index.ts b/packages/commandkit/tests/src/index.ts index a4a6945..3d35a48 100644 --- a/packages/commandkit/tests/src/index.ts +++ b/packages/commandkit/tests/src/index.ts @@ -15,7 +15,4 @@ new CommandKit({ bulkRegister: true, }); -console.log('io23hfio3h3iohgio3'); -throw new Error('oh no'); - await client.login(process.env.TOKEN); From 60ad57587876f71be020ca1946b2e54e5a6e4559 Mon Sep 17 00:00:00 2001 From: skdhg <46562212+skdhg@users.noreply.github.com> Date: Fri, 24 Nov 2023 21:15:43 +0545 Subject: [PATCH 5/5] chore: prettier --- packages/commandkit/bin/build.mjs | 43 +++++++++++++----------- packages/commandkit/bin/common.mjs | 16 +++++---- packages/commandkit/bin/development.mjs | 12 +++++-- packages/commandkit/bin/production.mjs | 35 ++++++++++++------- packages/commandkit/tests/commandkit.mjs | 2 +- 5 files changed, 67 insertions(+), 41 deletions(-) diff --git a/packages/commandkit/bin/build.mjs b/packages/commandkit/bin/build.mjs index 4b38bfe..f394c0d 100644 --- a/packages/commandkit/bin/build.mjs +++ b/packages/commandkit/bin/build.mjs @@ -7,7 +7,14 @@ import { appendFile } from 'node:fs/promises'; import { join } from 'node:path'; export async function bootstrapProductionBuild(config) { - const { sourcemap = false, minify = false, outDir = 'dist', antiCrash = true, src, main } = await findCommandKitConfig(config); + const { + sourcemap = false, + minify = false, + outDir = 'dist', + antiCrash = true, + src, + main, + } = await findCommandKitConfig(config); const status = ora('Creating optimized production build...\n').start(); const start = performance.now(); @@ -39,9 +46,7 @@ export async function bootstrapProductionBuild(config) { ); write( Colors.green( - `\nRun ${Colors.magenta(`commandkit start`)} ${Colors.green( - 'to start your bot.', - )}`, + `\nRun ${Colors.magenta(`commandkit start`)} ${Colors.green('to start your bot.')}`, ), ); } catch (e) { @@ -53,24 +58,24 @@ export async function bootstrapProductionBuild(config) { function injectAntiCrash(outDir, main) { const path = join(process.cwd(), outDir, main); const snippet = [ - "\n\n// --- CommandKit Anti-Crash Monitor ---", - ";(()=>{", + '\n\n// --- CommandKit Anti-Crash Monitor ---', + ';(()=>{', " 'use strict';", " // 'uncaughtException' event is supposed to be used to perform synchronous cleanup before shutting down the process", - " // instead of using it as a means to resume operation.", - " // But it exists here due to compatibility reasons with discord bot ecosystem.", + ' // instead of using it as a means to resume operation.', + ' // But it exists here due to compatibility reasons with discord bot ecosystem.', " const p = (t) => `\\x1b[33m${t}\\x1b[0m`, b = '[CommandKit Anti-Crash Monitor]', l = console.log, e1 = 'uncaughtException', e2 = 'unhandledRejection';", - " if (!process.eventNames().includes(e1)) // skip if it is already handled", - " process.on(e1, (e, o) => {", - " l(p(`${b} Uncaught Exception`)); l(p(b), p(e.stack || e));", - " })", - " if (!process.eventNames().includes(e2)) // skip if it is already handled", - " process.on(e2, (r) => {", - " l(p(`${b} Unhandled promise rejection`)); l(p(`${b} ${r.stack || r}`));", - " });", - "})();", - "// --- CommandKit Anti-Crash Monitor ---\n", + ' if (!process.eventNames().includes(e1)) // skip if it is already handled', + ' process.on(e1, (e, o) => {', + ' l(p(`${b} Uncaught Exception`)); l(p(b), p(e.stack || e));', + ' })', + ' if (!process.eventNames().includes(e2)) // skip if it is already handled', + ' process.on(e2, (r) => {', + ' l(p(`${b} Unhandled promise rejection`)); l(p(`${b} ${r.stack || r}`));', + ' });', + '})();', + '// --- CommandKit Anti-Crash Monitor ---\n', ].join('\n'); return appendFile(path, snippet); -} \ No newline at end of file +} diff --git a/packages/commandkit/bin/common.mjs b/packages/commandkit/bin/common.mjs index 4fd671c..3f1ca37 100644 --- a/packages/commandkit/bin/common.mjs +++ b/packages/commandkit/bin/common.mjs @@ -59,15 +59,19 @@ export function findPackageJSON() { } const possibleFileNames = [ - 'commandkit.json', 'commandkit.config.json', - 'commandkit.js', 'commandkit.config.js', - 'commandkit.mjs', 'commandkit.config.mjs', - 'commandkit.cjs', 'commandkit.config.cjs' + 'commandkit.json', + 'commandkit.config.json', + 'commandkit.js', + 'commandkit.config.js', + 'commandkit.mjs', + 'commandkit.config.mjs', + 'commandkit.cjs', + 'commandkit.config.cjs', ]; export async function findCommandKitConfig(src) { const cwd = process.cwd(); - const locations = src ? [join(cwd, src)] : possibleFileNames.map(name => join(cwd, name)); + const locations = src ? [join(cwd, src)] : possibleFileNames.map((name) => join(cwd, name)); for (const location of locations) { try { @@ -88,7 +92,7 @@ async function loadConfigInner(target) { */ const config = await import(`file://${target}`, { assert: isJSON ? { type: 'json' } : undefined, - }).then(conf => conf.default || conf); + }).then((conf) => conf.default || conf); return config; } diff --git a/packages/commandkit/bin/development.mjs b/packages/commandkit/bin/development.mjs index 7097832..3f5dd30 100644 --- a/packages/commandkit/bin/development.mjs +++ b/packages/commandkit/bin/development.mjs @@ -18,7 +18,7 @@ export async function bootstrapDevelopmentServer(opts) { nodeOptions = [], envExtra = true, clearRestartLogs = true, - outDir + outDir, } = await findCommandKitConfig(opts.config); if (!src) { @@ -107,7 +107,8 @@ export async function bootstrapDevelopmentServer(opts) { }, ); - let isLastLogRestarting = false, hasStarted = false; + let isLastLogRestarting = false, + hasStarted = false; ps.stdout.on('data', (data) => { const message = data.toString(); @@ -137,7 +138,12 @@ export async function bootstrapDevelopmentServer(opts) { ps.stderr.on('data', (data) => { const message = data.toString(); - if (message.includes('ExperimentalWarning: Watch mode is an experimental feature and might change at any time')) return; + if ( + message.includes( + 'ExperimentalWarning: Watch mode is an experimental feature and might change at any time', + ) + ) + return; write(Colors.red(message)); }); diff --git a/packages/commandkit/bin/production.mjs b/packages/commandkit/bin/production.mjs index 82d0680..6bb0c64 100644 --- a/packages/commandkit/bin/production.mjs +++ b/packages/commandkit/bin/production.mjs @@ -7,7 +7,12 @@ import { existsSync } from 'node:fs'; import { parseEnv } from './parse-env.mjs'; export async function bootstrapProductionServer(config) { - const { main, outDir = 'dist', envExtra = true, sourcemap } = await findCommandKitConfig(config); + const { + main, + outDir = 'dist', + envExtra = true, + sourcemap, + } = await findCommandKitConfig(config); if (!existsSync(join(process.cwd(), outDir, main))) { panic('Could not find production build, maybe run `commandkit build` first?'); @@ -37,18 +42,24 @@ export async function bootstrapProductionServer(config) { /** * @type {child_process.ChildProcessWithoutNullStreams} */ - const ps = child_process.spawn('node', [sourcemap ? '--enable-source-maps' : '', join(process.cwd(), outDir, main)].filter(Boolean), { - env: { - ...process.env, - ...processEnv, - NODE_ENV: 'production', - // @ts-expect-error - COMMANDKIT_DEV: false, - // @ts-expect-error - COMMANDKIT_PROD: true, + const ps = child_process.spawn( + 'node', + [sourcemap ? '--enable-source-maps' : '', join(process.cwd(), outDir, main)].filter( + Boolean, + ), + { + env: { + ...process.env, + ...processEnv, + NODE_ENV: 'production', + // @ts-expect-error + COMMANDKIT_DEV: false, + // @ts-expect-error + COMMANDKIT_PROD: true, + }, + cwd: process.cwd(), }, - cwd: process.cwd(), - }); + ); ps.stdout.on('data', (data) => { write(data.toString()); diff --git a/packages/commandkit/tests/commandkit.mjs b/packages/commandkit/tests/commandkit.mjs index 1b0a280..c895b61 100644 --- a/packages/commandkit/tests/commandkit.mjs +++ b/packages/commandkit/tests/commandkit.mjs @@ -5,4 +5,4 @@ import { defineConfig } from '../dist/index.mjs'; export default defineConfig({ main: 'index.mjs', src: 'src', -}); \ No newline at end of file +});