Skip to content

Commit

Permalink
feat!: dev env wip
Browse files Browse the repository at this point in the history
  • Loading branch information
twlite committed Feb 2, 2024
1 parent b3ef14f commit 9ff8593
Show file tree
Hide file tree
Showing 22 changed files with 414 additions and 57 deletions.
3 changes: 2 additions & 1 deletion packages/commandkit/bin/index.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/env node
require('source-map-support').install();

// require('source-map-support').install();
import('commandkit/cli');
1 change: 1 addition & 0 deletions packages/commandkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"event handler"
],
"dependencies": {
"chokidar": "^3.5.3",
"dotenv-cra": "^3.0.3",
"ora": "^8.0.1",
"rfdc": "^1.3.1",
Expand Down
121 changes: 119 additions & 2 deletions packages/commandkit/src/bootstrap/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
import { Client } from 'discord.js';
import { getConfig } from '../config';
import { watch } from 'chokidar';
import { CommandKitConfig, getConfig } from '../config';
import { CommandKit, Environment } from '..';
import { notification } from '../environment/actions/development';
import { CKitActionType, CKitNotification } from '../environment/actions/common';
import { join } from 'path';

let discord_client: Client;
let discord_client: Client, commandkit: CommandKit;

export const DisposableCallbacksRegistry = new Set<DisposableCallback>();

type DisposableCallback = () => Awaited<void>;

/**
* Registers a callback that will be executed when the the process is about restart. This is useful for cleaning up resources.
* This will only be executed in development mode.
*/
export function disposable(callback: DisposableCallback) {
const cb = async () => {
try {
await callback();
} finally {
DisposableCallbacksRegistry.delete(cb);
}
};

DisposableCallbacksRegistry.add(cb);
}

/**
* @internal
Expand All @@ -10,6 +35,22 @@ export function getClient() {
return discord_client;
}

/**
* @internal
*/
export function getCommandKit() {
return commandkit;
}

export function registerClient(client: Client) {
if (discord_client) return;
if (Environment.isDevelopment()) {
throw new Error('Cannot register client in development mode.');
}

discord_client = client;
}

/**
* Fetches the client instance. If the client instance is not initialized, an error will be thrown.
*/
Expand All @@ -30,3 +71,79 @@ export function createClient() {

return discord_client;
}

export function setupCommandKit(client: Client) {
const config = getConfig();

const getPath = (to: string) => join(process.cwd(), '.commandkit', to);

commandkit = new CommandKit({
...config.commandHandler,
commandsPath: getPath('commands'),
eventsPath: getPath('events'),
validationsPath: getPath('validations'),
client,
});

registerWatcher(commandkit, config);

return commandkit;
}

function registerWatcher(commandkit: CommandKit, config: CommandKitConfig) {
if (!config.watch) return;

// handles changes made to commands
_initCommandsWatcher(commandkit);
// handles changes made to events
_initEventsWatcher(commandkit);
// handles changes made to validations
_initValidationsWatcher(commandkit);
}

const _ignorable = (str: string) => /^_/.test(str) || /\.(map|d\.ts)$/.test(str);

function _initEventsWatcher(commandkit: CommandKit) {
if (!commandkit.eventsPath) return;

const watcher = watch(commandkit.eventsPath, {
ignoreInitial: true,
ignored: (testString) => _ignorable(testString),
});

watcher.on('all', async (event) => {
if (event === 'change') {
notification.emit(CKitNotification.Reload, CKitActionType.ReloadEvents);
}
});
}

function _initValidationsWatcher(commandkit: CommandKit) {
if (!commandkit.validationsPath) return;

const watcher = watch(commandkit.validationsPath, {
ignoreInitial: true,
ignored: (testString) => _ignorable(testString),
});

watcher.on('all', async (event) => {
if (event === 'change') {
notification.emit(CKitNotification.Reload, CKitActionType.ReloadValidators);
}
});
}

function _initCommandsWatcher(commandkit: CommandKit) {
if (!commandkit.commandsPath) return;

const watcher = watch(commandkit.commandsPath, {
ignoreInitial: true,
ignored: (testString) => _ignorable(testString),
});

watcher.on('all', async (event) => {
if (event === 'change') {
notification.emit(CKitNotification.Reload, CKitActionType.ReloadCommands);
}
});
}
5 changes: 4 additions & 1 deletion packages/commandkit/src/bootstrap/loadEnv.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { config } from 'dotenv-cra';
import { Environment } from '../environment/env';

export function loadEnv() {
const type = Environment.getType();

export function loadEnv(type: 'development' | 'production') {
process.env.NODE_ENV = type;

const result = config();
Expand Down
10 changes: 9 additions & 1 deletion packages/commandkit/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import type { ClientOptions } from 'discord.js';
import { CommandKitOptions } from './typings';

export interface CommandKitConfig {
/**
* The Discord client options.
*/
clientOptions: ClientOptions;
/**
* The command handler options.
*/
commandHandler?: Omit<
CommandKitOptions,
'client' | 'commandsPath' | 'validationsPath' | 'eventsPath'
>;
/**
* The Discord bot token. Defaults to `process.env.DISCORD_TOKEN`.
*/
Expand Down Expand Up @@ -57,7 +65,7 @@ let globalConfig: Partial<CommandKitConfig> = {
nodeOptions: [],
antiCrash: true,
requirePolyfill: true,
token: process.env.DISCORD_TOKEN,
token: undefined,
};

export function getConfig(): CommandKitConfig {
Expand Down
11 changes: 11 additions & 0 deletions packages/commandkit/src/environment/actions/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum CKitActionType {
ReloadAll,
ReloadCommands,
ReloadEvents,
ReloadValidators,
}

export enum CKitNotification {
Reload = 'reload',
ReloadAck = 'reload-ack',
}
66 changes: 57 additions & 9 deletions packages/commandkit/src/environment/actions/development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@ import { findConfigPath, importConfig } from '../common/config';
import colors from '../../utils/colors';
import { Logger } from '../common/logger';
import { loadEnv } from '../../bootstrap/loadEnv';
import { createClient, getClient } from '../../bootstrap/client';
import {
DisposableCallbacksRegistry,
createClient,
getClient,
getCommandKit,
setupCommandKit,
} from '../../bootstrap/client';
import { bundle } from '../bundler/bundle';
import { EventEmitter } from 'node:events';
import { CKitActionType, CKitNotification } from './common';
import { CKitInternalEnvState } from '../env';

const commandkitVersion = '[VI]{{inject}}[/VI]';

export const notification = new EventEmitter();

function printBanner() {
const banner = colors.magenta(`${String.fromCharCode(9670)} CommandKit ${commandkitVersion}`);
Logger.Log(banner);
Expand All @@ -27,7 +38,9 @@ export async function initializeDevelopmentEnvironment(
return;
}

const envErr = loadEnv('development');
CKitInternalEnvState.$env__type = 'development';

const envErr = loadEnv();

if (envErr) {
Logger.Warning('Failed to load .env', envErr);
Expand All @@ -40,8 +53,7 @@ export async function initializeDevelopmentEnvironment(
const msg = `Could not locate the commandkit config file${
args.config ? ' at ' + args.config : ' in the current working directory'
}.`;
Logger.Fatal(msg);
return;
return Logger.Fatal(msg);
}

try {
Expand All @@ -54,15 +66,51 @@ export async function initializeDevelopmentEnvironment(
}

const client = createClient();
setupCommandKit(client);

// build the project
const entrypoint = await bundle('development');
notification.once(CKitNotification.ReloadAck, async () => {
await client.login(config.token);
});

try {
// load the client entrypoint
await import(`file://${entrypoint}`);
await client.login(config.token);
notification.emit(CKitNotification.Reload, CKitActionType.ReloadAll);
} catch (e) {
Logger.Fatal('Failed to load the client entrypoint', e);
}
}

const ensureCommandKit = () => {
const commandkit = getCommandKit();

if (!commandkit) {
return Logger.Fatal('CommandKit is not initialized.');
}

return commandkit;
};

notification.on(CKitNotification.Reload, async (action: CKitActionType) => {
const entrypoint = await bundle();

try {
switch (action) {
case CKitActionType.ReloadAll:
// dispose before reloading to clean up any resources
await Promise.all([...DisposableCallbacksRegistry.values()].map((cb) => cb()));
await import(`file://${entrypoint}?t=${Date.now()}`);
notification.emit(CKitNotification.ReloadAck);
break;
case CKitActionType.ReloadCommands:
await ensureCommandKit().reloadCommands();
break;
case CKitActionType.ReloadEvents:
await ensureCommandKit().reloadEvents();
break;
case CKitActionType.ReloadValidators:
await ensureCommandKit().reloadValidations();
break;
}
} catch (e) {
Logger.Fatal('Failed to load the client entrypoint', e);
}
});
51 changes: 26 additions & 25 deletions packages/commandkit/src/environment/bundler/bundle.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
import { build } from 'tsup';
import { getConfig } from '../../config';
import { join } from 'path';
import { injectShims } from './shims';
import { Environment } from '../env';

export function bundle(mode: 'development' | 'production') {
switch (mode) {
case 'development':
export function bundle() {
switch (true) {
case Environment.isDevelopment():
return buildDevelopment();
default:
throw new Error('Not implemented');
}
}

function buildDevelopment() {
const { watch } = getConfig();
return new Promise<string>((resolve) => {
const { requirePolyfill } = getConfig();

const outDir = join(process.cwd(), '.commandkit');

return build({
clean: true,
format: ['esm'],
bundle: false,
dts: false,
skipNodeModulesBundle: true,
minify: false,
shims: true,
sourcemap: 'inline',
keepNames: true,
outDir: '.commandkit',
silent: true,
entry: ['src'],
watch,
async onSuccess() {
// return await injectShims('.commandkit', main, false, requirePolyfill);
},
}).then(() => {
return join(outDir, 'client.mjs');
return build({
clean: true,
format: ['esm'],
bundle: false,
dts: false,
skipNodeModulesBundle: true,
minify: false,
shims: true,
sourcemap: 'inline',
keepNames: true,
outDir: '.commandkit',
silent: true,
entry: ['src'],
async onSuccess() {
const root = join(process.cwd(), '.commandkit');
await injectShims(root, 'client.mjs', false, requirePolyfill);
resolve(join(root, 'client.mjs'));
},
});
});
}
Loading

0 comments on commit 9ff8593

Please sign in to comment.