Skip to content

Commit 2d0e9a4

Browse files
committedFeb 26, 2025·
refactor: update command router and cli
1 parent 4bdf3c9 commit 2d0e9a4

23 files changed

+422
-687
lines changed
 

‎apps/test-bot/commandkit.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import { defineConfig } from 'commandkit';
22

3-
export default defineConfig({});
3+
export default defineConfig();

‎packages/commandkit/src/app/router/CommandsRouter.ts

+111-53
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export interface ParsedCommand {
5151
middlewares: string[];
5252
/** Absolute path to this command */
5353
fullPath: string;
54+
/** Category the command belongs to, if any */
55+
category: string | null;
5456
}
5557

5658
/**
@@ -66,6 +68,8 @@ export interface ParsedMiddleware {
6668
path: string;
6769
/** Absolute path to the middleware file */
6870
fullPath: string;
71+
/** Category the middleware belongs to, if any */
72+
category: string | null;
6973
}
7074

7175
/**
@@ -244,68 +248,81 @@ export class CommandsRouter {
244248
* @returns Promise resolving to the complete commands tree
245249
*/
246250
public async scan(): Promise<CommandsTree> {
251+
this.clear();
247252
const files = await this.scanDirectory(this.entrypoint, []);
248253

249-
for (const file of files) {
250-
if (this.execMatcher(this.matchers.command, file)) {
251-
const location = this.resolveRelativePath(file);
252-
const parts = location.split(path.sep);
253-
254-
const parentSegments: string[] = [];
255-
256-
parts.forEach((part, index, arr) => {
257-
const isLast = index === arr.length - 1;
258-
259-
// ignore last because it's definitely a command source file
260-
if (isLast) return;
261-
262-
// we ignore groups
263-
if (!/\(.+\)/.test(part)) {
264-
parentSegments.push(part.trim());
265-
}
266-
});
267-
268-
const parent = parentSegments.join(' ');
269-
const name = parts[parts.length - 2];
270-
271-
const command: ParsedCommand = {
272-
name,
273-
middlewares: [],
274-
parent: parent || null,
275-
path: location,
276-
fullPath: file,
277-
parentSegments,
278-
};
254+
// First pass: collect all files
255+
const commandFiles = files.filter((file) => {
256+
const basename = path.basename(file);
257+
return !this.isIgnoredFile(basename) && this.isCommandFile(file);
258+
});
279259

280-
this.commands.set(name, command);
281-
}
260+
// Second pass: process middleware
261+
const middlewareFiles = files.filter((file) =>
262+
this.execMatcher(this.matchers.middleware, file),
263+
);
264+
265+
// Process commands
266+
for (const file of commandFiles) {
267+
const parsedPath = this.parseCommandPath(file);
268+
const location = this.resolveRelativePath(file);
269+
270+
const command: ParsedCommand = {
271+
name: parsedPath.name,
272+
path: location,
273+
fullPath: file,
274+
parent: parsedPath.parent,
275+
parentSegments: parsedPath.parentSegments,
276+
category: parsedPath.category,
277+
middlewares: [],
278+
};
282279

283-
if (this.execMatcher(this.matchers.middleware, file)) {
284-
const location = this.resolveRelativePath(file);
285-
const name = location.replace(/\.(m|c)?(j|t)sx?$/, '');
286-
const middlewareDir = path.dirname(location);
280+
this.commands.set(parsedPath.name, command);
281+
}
287282

288-
const command = Array.from(this.commands.values()).filter((command) => {
289-
const commandDir = path.dirname(command.path);
290-
return (
291-
commandDir === middlewareDir || commandDir.startsWith(middlewareDir)
292-
);
293-
});
283+
// Process middleware
284+
for (const file of middlewareFiles) {
285+
const location = this.resolveRelativePath(file);
286+
const dirname = path.dirname(location);
287+
const id = crypto.randomUUID();
288+
const parts = location.split(path.sep).filter((p) => p);
289+
const categories = this.parseCategories(parts);
290+
291+
const middleware: ParsedMiddleware = {
292+
id,
293+
name: dirname,
294+
path: location,
295+
fullPath: file,
296+
category: categories.length ? categories.join('/') : null,
297+
};
294298

295-
const id = crypto.randomUUID();
299+
this.middlewares.set(id, middleware);
296300

297-
const middleware: ParsedMiddleware = {
298-
id,
299-
name,
300-
path: location,
301-
fullPath: file,
302-
};
301+
// Apply middleware based on location
302+
const isGlobalMiddleware = path.parse(file).name === 'middleware';
303+
const commands = Array.from(this.commands.values());
303304

304-
this.middlewares.set(id, middleware);
305+
for (const command of commands) {
306+
const commandDir = path.dirname(command.path);
305307

306-
command.forEach((cmd) => {
307-
cmd.middlewares.push(id);
308-
});
308+
if (isGlobalMiddleware) {
309+
// Global middleware applies if command is in same dir or nested
310+
if (
311+
commandDir === dirname ||
312+
commandDir.startsWith(dirname + path.sep)
313+
) {
314+
command.middlewares.push(id);
315+
}
316+
} else {
317+
// Specific middleware only applies to exact command match
318+
const commandName = command.name;
319+
const middlewareName = path
320+
.basename(file)
321+
.replace(/\.middleware\.(m|c)?(j|t)sx?$/, '');
322+
if (commandName === middlewareName && commandDir === dirname) {
323+
command.middlewares.push(id);
324+
}
325+
}
309326
}
310327
}
311328

@@ -357,4 +374,45 @@ export class CommandsRouter {
357374

358375
return entries;
359376
}
377+
378+
private isIgnoredFile(filename: string): boolean {
379+
return filename.startsWith('_');
380+
}
381+
382+
private isCommandFile(path: string): boolean {
383+
if (this.execMatcher(this.matchers.middleware, path)) return false;
384+
return (
385+
/index\.(m|c)?(j|t)sx?$/.test(path) || /\.(m|c)?(j|t)sx?$/.test(path)
386+
);
387+
}
388+
389+
private parseCategories(parts: string[]): string[] {
390+
return parts
391+
.filter((part) => part.startsWith('(') && part.endsWith(')'))
392+
.map((part) => part.slice(1, -1));
393+
}
394+
395+
private parseCommandPath(filepath: string): {
396+
name: string;
397+
category: string | null;
398+
parent: string | null;
399+
parentSegments: string[];
400+
} {
401+
const location = this.resolveRelativePath(filepath);
402+
const parts = location.split(path.sep).filter((p) => p);
403+
const categories = this.parseCategories(parts);
404+
const segments: string[] = parts.filter(
405+
(part) => !(part.startsWith('(') && part.endsWith(')')),
406+
);
407+
408+
let name = segments.pop() || '';
409+
name = name.replace(/\.(m|c)?(j|t)sx?$/, '').replace(/^index$/, '');
410+
411+
return {
412+
name,
413+
category: categories.length ? categories.join('/') : null,
414+
parent: segments.length ? segments.join(' ') : null,
415+
parentSegments: segments,
416+
};
417+
}
360418
}

‎packages/commandkit/src/cli/build.ts

+52-175
Original file line numberDiff line numberDiff line change
@@ -1,182 +1,59 @@
1-
// @ts-check
2-
3-
import { readFile, writeFile } from 'node:fs/promises';
4-
import { join } from 'node:path';
51
import { build } from 'tsup';
62
import {
7-
copyLocaleFiles,
8-
erase,
9-
findCommandKitConfig,
10-
panic,
11-
write,
12-
} from './common.js';
13-
import colors from '../utils/colors.js';
14-
import { createSpinner } from './utils';
15-
import { BuildOptions } from './types';
16-
import { CompilerPluginRuntime } from '../plugins/runtime/CompilerPluginRuntime.js';
17-
import { isCompilerPlugin } from '../plugins/CompilerPlugin.js';
18-
19-
export async function bootstrapProductionBuild(configPath: string) {
20-
const config = await findCommandKitConfig(configPath);
21-
const spinner = await createSpinner('Creating optimized production build...');
22-
const start = performance.now();
23-
24-
try {
25-
await buildProject(config);
26-
spinner.succeed(
27-
colors.green(
28-
`Build completed in ${(performance.now() - start).toFixed(2)}ms!`,
29-
),
30-
);
31-
} catch (e) {
32-
spinner.fail('Build failed');
33-
panic(e instanceof Error ? e.stack : e);
34-
}
3+
CompilerPlugin,
4+
CompilerPluginRuntime,
5+
fromEsbuildPlugin,
6+
} from '../plugins';
7+
import { loadConfigFile } from '../config/loader';
8+
9+
export interface ApplicationBuildOptions {
10+
plugins?: CompilerPlugin[];
11+
esbuildPlugins?: any[];
12+
isDev?: boolean;
13+
configPath?: string;
3514
}
3615

37-
export async function bootstrapDevelopmentBuild(configPath: string) {
38-
const config = await findCommandKitConfig(configPath);
39-
40-
try {
41-
await buildProject({
42-
...config,
43-
outDir: '.commandkit',
44-
isDevelopment: true,
45-
});
46-
} catch (e) {
47-
console.error(e instanceof Error ? e.stack : e);
48-
console.error(
49-
colors.red('Failed to build the project. Waiting for changes...'),
50-
);
16+
export async function buildApplication({
17+
plugins,
18+
esbuildPlugins,
19+
isDev,
20+
configPath,
21+
}: ApplicationBuildOptions) {
22+
const config = await loadConfigFile(configPath);
23+
const pluginRuntime = new CompilerPluginRuntime(plugins || []);
24+
const esbuildPluginList: any[] = pluginRuntime.isEmpty()
25+
? []
26+
: [pluginRuntime];
27+
28+
if (esbuildPlugins?.length) {
29+
esbuildPluginList.push(...esbuildPlugins.map(fromEsbuildPlugin));
5130
}
52-
}
53-
54-
async function buildProject(options: BuildOptions) {
55-
const {
56-
sourcemap = false,
57-
minify = false,
58-
outDir = 'dist',
59-
antiCrash = true,
60-
main,
61-
requirePolyfill: polyfillRequire,
62-
} = options;
63-
64-
const config = await findCommandKitConfig(process.cwd());
65-
66-
erase(outDir);
67-
68-
const compilerPlugins = config.plugins.filter((p) => !isCompilerPlugin(p));
69-
70-
try {
71-
await build({
72-
clean: true,
73-
format: ['esm'],
74-
dts: false,
75-
skipNodeModulesBundle: true,
76-
minify,
77-
shims: true,
78-
banner: options.isDevelopment
79-
? {}
80-
: {
81-
js: '/* Optimized production build generated by CommandKit */',
82-
},
83-
sourcemap,
84-
keepNames: true,
85-
outDir,
86-
silent: true,
87-
watch: !!options.isDevelopment && !!options.watch,
88-
cjsInterop: true,
89-
splitting: true,
90-
entry: ['src', '!dist', '!.commandkit', `!${outDir}`],
91-
esbuildPlugins: [
92-
// @ts-ignore
93-
new CompilerPluginRuntime(compilerPlugins),
94-
],
95-
jsxFactory: 'CommandKit.createElement',
96-
jsxFragment: 'CommandKit.Fragment',
97-
async onSuccess() {
98-
await copyLocaleFiles('src', outDir);
99-
},
100-
});
101-
102-
await injectShims(
103-
outDir,
104-
main,
105-
!options.isDevelopment && antiCrash,
106-
!!polyfillRequire,
107-
);
108-
109-
if (!options.isDevelopment) {
110-
write(
111-
colors.green(
112-
`\nRun ${colors.magenta(`commandkit start`)} ${colors.green(
113-
'to start your bot.',
114-
)}`,
115-
),
116-
);
117-
}
118-
} catch (e) {
119-
panic(e);
120-
}
121-
}
122-
123-
export async function injectShims(
124-
outDir: string,
125-
main: string,
126-
antiCrash: boolean,
127-
polyfillRequire: boolean,
128-
) {
129-
const path = join(process.cwd(), outDir, main);
130-
131-
const head = ['\n\n;await (async()=>{', " 'use strict';"].join('\n');
132-
const tail = '\n})();';
133-
const requireScript = polyfillRequire
134-
? [
135-
'// --- CommandKit require() polyfill ---',
136-
' if (typeof require === "undefined") {',
137-
' const { createRequire } = await import("node:module");',
138-
' const __require = createRequire(import.meta.url);',
139-
' Object.defineProperty(globalThis, "require", {',
140-
' value: (id) => {',
141-
' return __require(id);',
142-
' },',
143-
' configurable: true,',
144-
' enumerable: false,',
145-
' writable: true,',
146-
' });',
147-
' }',
148-
'// --- CommandKit require() polyfill ---',
149-
].join('\n')
150-
: '';
151-
152-
const antiCrashScript = antiCrash
153-
? [
154-
'// --- CommandKit Anti-Crash Monitor ---',
155-
" // 'uncaughtException' event is supposed to be used to perform synchronous cleanup before shutting down the process",
156-
' // instead of using it as a means to resume operation.',
157-
' // But it exists here due to compatibility reasons with discord bot ecosystem.',
158-
" const p = (t) => `\\x1b[33m${t}\\x1b[0m`, b = '[CommandKit Anti-Crash Monitor]', l = console.log, e1 = 'uncaughtException', e2 = 'unhandledRejection';",
159-
' if (!process.eventNames().includes(e1)) // skip if it is already handled',
160-
' process.on(e1, (e) => {',
161-
' l(p(`${b} Uncaught Exception`)); l(p(b), p(e.stack || e));',
162-
' })',
163-
' if (!process.eventNames().includes(e2)) // skip if it is already handled',
164-
' process.on(e2, (r) => {',
165-
' l(p(`${b} Unhandled promise rejection`)); l(p(`${b} ${r.stack || r}`));',
166-
' });',
167-
'// --- CommandKit Anti-Crash Monitor ---',
168-
].join('\n')
169-
: '';
170-
171-
const contents = await readFile(path, 'utf-8');
172-
const finalScript = [
173-
head,
174-
requireScript,
175-
antiCrashScript,
176-
tail,
177-
'\n\n',
178-
contents,
179-
].join('\n');
18031

181-
return writeFile(path, finalScript);
32+
await build({
33+
esbuildPlugins: esbuildPluginList,
34+
watch: !!isDev,
35+
banner: {
36+
js: !isDev
37+
? '/* Optimized production build generated by commandkit */'
38+
: '',
39+
},
40+
cjsInterop: true,
41+
dts: false,
42+
clean: true,
43+
format: ['esm'],
44+
shims: true,
45+
keepNames: true,
46+
minify: false,
47+
jsxFactory: 'CommandKit.createElement',
48+
jsxFragment: 'CommandKit.Fragment',
49+
minifyIdentifiers: false,
50+
minifySyntax: false,
51+
silent: !!isDev,
52+
splitting: true,
53+
skipNodeModulesBundle: true,
54+
name: 'CommandKit',
55+
sourcemap: true,
56+
target: 'node16',
57+
outDir: config.distDir,
58+
});
18259
}

‎packages/commandkit/src/cli/common.ts

+5-31
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
// @ts-check
2-
31
import { rimrafSync } from 'rimraf';
42
import { join } from 'node:path';
53
import fs from 'node:fs';
64
import colors from '../utils/colors';
7-
import { ResolvedCommandKitConfig } from '../config';
5+
import { ResolvedCommandKitConfig } from '../config/utils';
86

97
let ts: typeof import('typescript') | undefined;
108

@@ -32,30 +30,6 @@ export function findPackageJSON() {
3230
return JSON.parse(fs.readFileSync(target, 'utf8'));
3331
}
3432

35-
const possibleFileNames = [
36-
'commandkit.js',
37-
'commandkit.mjs',
38-
'commandkit.cjs',
39-
'commandkit.ts',
40-
];
41-
42-
export async function findCommandKitConfig(src: string) {
43-
const cwd = process.cwd();
44-
const locations = src
45-
? [join(cwd, src)]
46-
: possibleFileNames.map((name) => join(cwd, name));
47-
48-
for (const location of locations) {
49-
try {
50-
return await loadConfigInner(location);
51-
} catch (e) {
52-
continue;
53-
}
54-
}
55-
56-
panic(`Could not locate commandkit config from ${cwd}`);
57-
}
58-
5933
async function ensureTypeScript(target: string) {
6034
const isTypeScript = /\.(c|m)?tsx?$/.test(target);
6135

@@ -73,7 +47,7 @@ async function ensureTypeScript(target: string) {
7347
return true;
7448
}
7549

76-
async function loadConfigInner(
50+
export async function loadConfigFileFromPath(
7751
target: string,
7852
): Promise<ResolvedCommandKitConfig> {
7953
await ensureExists(target);
@@ -110,9 +84,9 @@ async function loadConfigInner(
11084
/**
11185
* @type {import('..').CommandKitConfig}
11286
*/
113-
const config = await import(`file://${target}`)
114-
.then((conf) => conf.default || conf)
115-
.catch(console.log);
87+
const config = await import(`file://${target}`).then(
88+
(conf) => conf.default || conf,
89+
);
11690

11791
return config;
11892
}
+1-94
Original file line numberDiff line numberDiff line change
@@ -1,94 +1 @@
1-
import { erase, findCommandKitConfig, panic, write } from './common';
2-
import colors from '../utils/colors';
3-
import { createNodeProcess, createSpinner } from './utils';
4-
import { bootstrapDevelopmentBuild } from './build';
5-
6-
const RESTARTING_MSG_PATTERN = /^Restarting '|".+'|"\n?$/;
7-
const FAILED_RUNNING_PATTERN = /^Failed running '.+'|"\n?$/;
8-
9-
export async function bootstrapDevelopmentServer(opts: any) {
10-
const config = await findCommandKitConfig(opts.config);
11-
const { watch = true, nodeOptions = [], clearRestartLogs = true } = config;
12-
13-
const spinner = await createSpinner('Starting development server...');
14-
const start = performance.now();
15-
16-
try {
17-
await erase('.commandkit');
18-
await bootstrapDevelopmentBuild(opts.config);
19-
20-
const ps = createNodeProcess({
21-
...config,
22-
outDir: '.commandkit',
23-
nodeOptions: [
24-
...nodeOptions,
25-
watch ? '--watch' : '',
26-
'--enable-source-maps',
27-
].filter(Boolean),
28-
env: {
29-
NODE_ENV: 'development',
30-
COMMANDKIT_DEV: 'true',
31-
COMMANDKIT_PRODUCTION: 'false',
32-
},
33-
});
34-
35-
let isLastLogRestarting = false,
36-
hasStarted = false;
37-
38-
ps.stdout?.on('data', (data) => {
39-
const message = data.toString();
40-
41-
if (FAILED_RUNNING_PATTERN.test(message)) {
42-
write(colors.cyan('Failed running the bot, waiting for changes...'));
43-
isLastLogRestarting = false;
44-
if (!hasStarted) hasStarted = true;
45-
return;
46-
}
47-
48-
if (clearRestartLogs && !RESTARTING_MSG_PATTERN.test(message)) {
49-
write(message);
50-
isLastLogRestarting = false;
51-
} else {
52-
if (isLastLogRestarting || !hasStarted) {
53-
if (!hasStarted) hasStarted = true;
54-
return;
55-
}
56-
write(colors.cyan('⌀ Restarting the bot...'));
57-
isLastLogRestarting = true;
58-
}
59-
60-
if (!hasStarted) hasStarted = true;
61-
});
62-
63-
ps.stderr?.on('data', (data) => {
64-
const message = data.toString();
65-
66-
if (
67-
message.includes(
68-
'ExperimentalWarning: Watch mode is an experimental feature and might change at any time',
69-
)
70-
)
71-
return;
72-
73-
write(colors.red(message));
74-
});
75-
76-
ps.on('close', (code) => {
77-
write('\n');
78-
process.exit(code ?? 0);
79-
});
80-
81-
ps.on('error', (err) => {
82-
panic(err);
83-
});
84-
85-
spinner.succeed(
86-
colors.green(
87-
`Dev server started in ${(performance.now() - start).toFixed(2)}ms!\n`,
88-
),
89-
);
90-
} catch (e) {
91-
spinner.fail(colors.red(`Failed to start dev server: ${e}`));
92-
panic(e instanceof Error ? e.stack : e);
93-
}
94-
}
1+
export async function bootstrapDevelopmentServer() {}

‎packages/commandkit/src/cli/init.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export async function bootstrapCommandkitCLI(
1010
const { Command } = await import('commander');
1111
const { bootstrapDevelopmentServer } = await import('./development');
1212
const { bootstrapProductionServer } = await import('./production');
13-
const { bootstrapProductionBuild } = await import('./build');
13+
const { buildApplication } = await import('./build');
1414
const { generateCommand, generateEvent, generateLocale } = await import(
1515
'./generators'
1616
);
@@ -55,7 +55,7 @@ export async function bootstrapCommandkitCLI(
5555
)
5656
.action(() => {
5757
const options = program.opts();
58-
bootstrapProductionBuild(options.config);
58+
buildApplication({ configPath: options.config });
5959
});
6060

6161
program

‎packages/commandkit/src/cli/parse-env.ts

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// @ts-check
2-
31
import { randomUUID } from 'node:crypto';
42

53
const valuesMap = {

‎packages/commandkit/src/cli/production.ts

-72
This file was deleted.

‎packages/commandkit/src/cli/types.ts

-16
This file was deleted.

‎packages/commandkit/src/cli/utils.ts

-71
This file was deleted.

‎packages/commandkit/src/config.ts

-152
This file was deleted.
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { defaultConfig } from './default';
2+
import { CommandKitConfig } from './types';
3+
import { mergeDeep, ResolvedCommandKitConfig } from './utils';
4+
5+
let defined: ResolvedCommandKitConfig = defaultConfig;
6+
7+
/**
8+
* Get the defined configuration for CommandKit.
9+
*/
10+
export function getConfig(): ResolvedCommandKitConfig {
11+
return defined;
12+
}
13+
14+
/**
15+
* Define the configuration for CommandKit.
16+
* @param config The configuration to use.
17+
*/
18+
export function defineConfig(
19+
config: Partial<CommandKitConfig> = {},
20+
): ResolvedCommandKitConfig {
21+
defined = mergeDeep(
22+
config as ResolvedCommandKitConfig,
23+
mergeDeep({} as ResolvedCommandKitConfig, defaultConfig),
24+
);
25+
26+
return defined;
27+
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { CachePlugin } from '../plugins/runtime/builtin/cache/CachePlugin';
2+
import { MacroPlugin } from '../plugins/runtime/builtin/MacroPlugin';
3+
import { ResolvedCommandKitConfig } from './utils';
4+
5+
export const defaultConfig: ResolvedCommandKitConfig = {
6+
plugins: [new CachePlugin({}), new MacroPlugin({})],
7+
esbuildPlugins: [],
8+
compilerOptions: {
9+
macro: {
10+
development: false,
11+
},
12+
cache: {
13+
development: true,
14+
},
15+
},
16+
static: true,
17+
typescript: {
18+
ignoreDuringBuilds: false,
19+
},
20+
distDir: '.commandkit',
21+
env: {},
22+
sourceMap: {
23+
development: true,
24+
production: true,
25+
},
26+
typedCommands: true,
27+
typedLocales: true,
28+
};
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { existsSync } from 'node:fs';
2+
import { join } from 'node:path';
3+
import { loadConfigFileFromPath } from '../cli/common';
4+
import { defaultConfig } from './default';
5+
import { getConfig } from './config';
6+
7+
const CONFIG_FILE_NAMES = [
8+
'commandkit.config.js',
9+
'commandkit.config.mjs',
10+
'commandkit.config.cjs',
11+
'commandkit.config.ts',
12+
];
13+
14+
function findConfigFile(cwd: string) {
15+
const locations = CONFIG_FILE_NAMES.map((name) => join(cwd, name));
16+
17+
for (const location of locations) {
18+
if (existsSync(location)) {
19+
return {
20+
path: location,
21+
isTypeScript: /\.ts$/.test(location),
22+
};
23+
}
24+
}
25+
26+
return null;
27+
}
28+
29+
let loadedConfig: ReturnType<typeof getConfig> | null = null;
30+
31+
/**
32+
* Load the configuration file from the given entrypoint.
33+
* @param entrypoint The entrypoint to load the configuration file from. Defaults to the current working directory.
34+
*/
35+
export async function loadConfigFile(entrypoint = process.cwd()) {
36+
if (loadedConfig) return loadedConfig;
37+
const filePath = findConfigFile(entrypoint);
38+
if (!filePath) return getConfig();
39+
40+
const config = await loadConfigFileFromPath(filePath.path);
41+
42+
loadedConfig = config;
43+
44+
return config;
45+
}
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { CommandKitPlugin } from '../plugins';
2+
3+
export interface CommandKitConfig {
4+
/**
5+
* The plugins to use with CommandKit.
6+
*/
7+
plugins?: CommandKitPlugin[];
8+
/**
9+
* The esbuild plugins to use with CommandKit.
10+
*/
11+
esbuildPlugins?: any[];
12+
/**
13+
* The compiler options to use with CommandKit.
14+
*/
15+
compilerOptions?: {
16+
/**
17+
* Macro compiler configuration.
18+
*/
19+
macro?: {
20+
/**
21+
* Whether to enable macro function compilation in development mode.
22+
* @default false
23+
*/
24+
development?: boolean;
25+
};
26+
/**
27+
* Cached function compiler configuration.
28+
*/
29+
cache?: {
30+
/**
31+
* Whether to enable caching of compiled functions in development mode.
32+
*/
33+
development?: boolean;
34+
};
35+
};
36+
/**
37+
* The typescript configuration to use with CommandKit.
38+
*/
39+
typescript?: {
40+
/**
41+
* Whether to ignore type checking during builds.
42+
*/
43+
ignoreDuringBuilds?: boolean;
44+
};
45+
/**
46+
* Whether to generate static command handler data in production builds.
47+
*/
48+
static?: boolean;
49+
/**
50+
* Statically define the environment variables to use.
51+
*/
52+
env?: Record<string, string>;
53+
/**
54+
* The custom build directory name to use.
55+
* @default `dist`
56+
*/
57+
distDir?: string;
58+
/**
59+
* Whether or not to enable the source map generation.
60+
*/
61+
sourceMap?: {
62+
/**
63+
* Whether to enable source map generation in development mode.
64+
*/
65+
development?: boolean;
66+
/**
67+
* Whether to enable source map generation in production mode.
68+
*/
69+
production?: boolean;
70+
};
71+
/**
72+
* Whether or not to enable typed locales.
73+
* @default true
74+
*/
75+
typedLocales?: boolean;
76+
/**
77+
* Whether or not to enable the typed commands.
78+
* @default true
79+
*/
80+
typedCommands?: boolean;
81+
}
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { CommandKitConfig } from './types';
2+
3+
export type DeepRequired<T> = {
4+
[P in keyof T]-?: DeepRequired<T[P]>;
5+
};
6+
7+
export type DeepPartial<T> = {
8+
[P in keyof T]?: DeepPartial<T[P]>;
9+
};
10+
11+
export const mergeDeep = <T extends Record<string, any>>(
12+
target: T,
13+
source: T,
14+
): T => {
15+
const isObject = (obj: unknown) =>
16+
obj && typeof obj === 'object' && !Array.isArray(obj);
17+
18+
const output: T = { ...target };
19+
if (isObject(target) && isObject(source)) {
20+
Object.keys(source).forEach((key) => {
21+
if (isObject(source[key])) {
22+
if (!(key in target)) {
23+
Object.assign(output, { [key]: source[key] });
24+
} else {
25+
output[key as keyof T] = mergeDeep(target[key], source[key]);
26+
}
27+
} else {
28+
Object.assign(output, { [key]: source[key] });
29+
}
30+
});
31+
}
32+
return output as T;
33+
};
34+
35+
export type ResolvedCommandKitConfig = DeepRequired<CommandKitConfig>;

‎packages/commandkit/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default CommandKit;
44

55
export * from './CommandKit';
66
export * from './components';
7-
export * from './config';
7+
export * from './config/config';
88
export * from './context/async-context';
99
export * from './context/environment';
1010
export * from './cache/index';

‎packages/commandkit/src/plugins/PluginCommon.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CommonPluginRuntime } from './runtime/runtime';
33
export type PluginOptions = Record<string, any>;
44

55
export abstract class PluginCommon<
6-
T extends PluginOptions,
6+
T extends PluginOptions = PluginOptions,
77
C extends CommonPluginRuntime = CommonPluginRuntime,
88
> {
99
public abstract readonly name: string;

‎packages/commandkit/src/plugins/runtime/CompilerPluginRuntime.ts

+28-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { readFile } from 'node:fs/promises';
2-
import type { CompilerPlugin, MaybeFalsey, TransformedResult } from '..';
2+
import { CompilerPlugin, MaybeFalsey, TransformedResult } from '..';
33
import {
44
OnLoadArgs,
55
OnLoadResult,
66
OnResolveArgs,
77
OnResolveResult,
88
Setup,
99
} from './types';
10-
import { findCommandKitConfig } from '../../cli/common';
1110

1211
const pattern = /\.(c|m)?(t|j)sx?$/;
1312

@@ -16,8 +15,8 @@ export class CompilerPluginRuntime {
1615

1716
public constructor(private readonly plugins: CompilerPlugin[]) {}
1817

19-
public getConfig() {
20-
return findCommandKitConfig(process.cwd());
18+
public isEmpty() {
19+
return !this.plugins.length;
2120
}
2221

2322
private async onLoad(args: OnLoadArgs): Promise<OnLoadResult> {
@@ -167,3 +166,28 @@ export class CompilerPluginRuntime {
167166
await build.onDispose(this.onDispose.bind(this));
168167
}
169168
}
169+
170+
export function fromEsbuildPlugin(plugin: any): typeof CompilerPlugin {
171+
class EsbuildPluginCompat extends CompilerPlugin {
172+
public readonly name = plugin.name;
173+
174+
public onBuildEnd(): Promise<void> {
175+
return plugin.onBuildEnd?.();
176+
}
177+
178+
public onBuildStart(): Promise<void> {
179+
return plugin.onBuildStart?.();
180+
}
181+
182+
public async transform(params: any) {
183+
return plugin.transform?.(params);
184+
}
185+
186+
public async resolve(params: any) {
187+
return plugin.resolve?.(params);
188+
}
189+
}
190+
191+
// @ts-ignore
192+
return EsbuildPluginCompat;
193+
}

‎packages/commandkit/src/plugins/runtime/builtin/MacroPlugin.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@ import {
66
TransformedResult,
77
} from '../..';
88

9-
export interface MacroPluginOptions {
10-
enabled: boolean;
11-
}
12-
13-
export class MacroPlugin extends CompilerPlugin<MacroPluginOptions> {
9+
export class MacroPlugin extends CompilerPlugin {
1410
public readonly name = 'MacroPlugin';
1511

1612
private macroTransformer!: MacroTransformer;

‎packages/commandkit/src/plugins/runtime/builtin/cache/CachePlugin.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@ import {
66
} from '../../..';
77
import { cacheDirectivePlugin } from './UseCacheTransformer';
88

9-
export interface CachePluginOptions {
10-
enabled: boolean;
11-
}
12-
13-
export class CachePlugin extends CompilerPlugin<CachePluginOptions> {
9+
export class CachePlugin extends CompilerPlugin {
1410
public readonly name = 'CachePlugin';
1511

1612
public async transform(

‎packages/commandkit/tsup.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default defineConfig({
1111
keepNames: true,
1212
dts: true,
1313
shims: true,
14-
splitting: false,
14+
splitting: true,
1515
skipNodeModulesBundle: true,
1616
clean: true,
1717
target: 'node16',

‎packages/commandkit/vitest.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineConfig } from 'vitest/config';
2-
import { cacheDirectivePlugin } from './src/cli/esbuild-plugins/use-cache';
2+
import { cacheDirectivePlugin } from './src/plugins/runtime/builtin/cache/UseCacheTransformer';
33
import { join } from 'path';
44

55
export default defineConfig({

0 commit comments

Comments
 (0)
Please sign in to comment.