Skip to content

Commit 67cf4d3

Browse files
committed
feat: tasks plugin wip
1 parent 1d7d403 commit 67cf4d3

26 files changed

+711
-185
lines changed

packages/commandkit/src/CommandKit.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { join } from 'node:path';
1111
import { AppCommandHandler } from './app/handlers/AppCommandHandler';
1212
import { CommandsRouter, EventsRouter } from './app/router';
1313
import { AppEventsHandler } from './app/handlers/AppEventsHandler';
14-
import { CommandKitPluginRuntime } from './plugins/runtime/CommandKitPluginRuntime';
14+
import { CommandKitPluginRuntime } from './plugins/plugin-runtime/CommandKitPluginRuntime';
1515
import { loadConfigFile } from './config/loader';
1616
import { COMMANDKIT_IS_DEV } from './utils/constants';
1717
import { registerDevHooks } from './utils/dev-hooks';

packages/commandkit/src/config/default.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { CachePlugin } from '../plugins/runtime/builtin/cache/CachePlugin';
2-
import { MacroPlugin } from '../plugins/runtime/builtin/MacroPlugin';
1+
import { CachePlugin } from '../plugins/plugin-runtime/builtin/cache/CachePlugin';
2+
import { MacroPlugin } from '../plugins/plugin-runtime/builtin/MacroPlugin';
33
import { ResolvedCommandKitConfig } from './utils';
44

55
export const defaultConfig: ResolvedCommandKitConfig = {

packages/commandkit/src/plugins/CompilerPlugin.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import {
33
OnLoadArgs,
44
OnResolveArgs,
55
OnResolveResult,
6-
} from './runtime/types';
6+
} from './plugin-runtime/types';
77
import { PluginCommon, PluginOptions } from './PluginCommon';
88
import { MaybeFalsey } from './types';
9-
import { CompilerPluginRuntime } from './runtime/CompilerPluginRuntime';
9+
import { CompilerPluginRuntime } from './plugin-runtime/CompilerPluginRuntime';
1010

1111
export interface PluginTransformParameters {
1212
args: OnLoadArgs;

packages/commandkit/src/plugins/PluginCommon.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CommonPluginRuntime } from './runtime/runtime';
1+
import { CommonPluginRuntime } from './plugin-runtime/runtime';
22

33
export type PluginOptions = Record<string, any>;
44

packages/commandkit/src/plugins/RuntimePlugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Interaction, Message, PartialMessage } from 'discord.js';
22
import { PluginCommon, PluginOptions } from './PluginCommon';
3-
import type { CommandKitPluginRuntime } from './runtime/CommandKitPluginRuntime';
3+
import type { CommandKitPluginRuntime } from './plugin-runtime/CommandKitPluginRuntime';
44
import { CommandBuilderLike, PreparedAppCommandExecution } from '../app';
55
import { CommandKitEnvironment } from '../context/environment';
66
import { CommandKitHMREvent } from '../utils/dev-hooks';
+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export * from './CompilerPlugin';
22
export * from './RuntimePlugin';
33
export * from './types';
4-
export * from './runtime/CommandKitPluginRuntime';
5-
export * from './runtime/CompilerPluginRuntime';
6-
export * from './runtime/types';
7-
export * from './runtime/runtime';
4+
export * from './plugin-runtime/CommandKitPluginRuntime';
5+
export * from './plugin-runtime/CompilerPluginRuntime';
6+
export * from './plugin-runtime/types';
7+
export * from './plugin-runtime/runtime';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createDirectiveTransformer } from '../common/directive-transformer';
2+
3+
export const cacheDirectivePlugin = createDirectiveTransformer({
4+
directive: 'use cache',
5+
importPath: 'commandkit',
6+
importName: 'zxcvbnm____',
7+
asyncOnly: true,
8+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import * as parser from '@babel/parser';
2+
import _traverse from '@babel/traverse';
3+
import _generate from '@babel/generator';
4+
import * as t from '@babel/types';
5+
6+
// @ts-ignore
7+
const traverse = _traverse.default || _traverse;
8+
// @ts-ignore
9+
const generate = _generate.default || _generate;
10+
11+
const generateRandomString = (length = 6) => {
12+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
13+
return Array.from(
14+
{ length },
15+
() => chars[Math.floor(Math.random() * chars.length)],
16+
).join('');
17+
};
18+
19+
export interface DirectiveTransformerOptions {
20+
/**
21+
* The directive to look for in the code.
22+
*/
23+
directive: string;
24+
/**
25+
* The path to the module to import from.
26+
*/
27+
importPath: string;
28+
/**
29+
* The name of the import to use.
30+
*/
31+
importName: string;
32+
/**
33+
* Whether to only allow async functions.
34+
*/
35+
asyncOnly?: boolean;
36+
}
37+
38+
/**
39+
* Creates a transformer that looks for a specific directive in the code and rewrites it accordingly by
40+
* wrapping the function in a call to the imported higher-order function.
41+
* @param options The options for the transformer.
42+
* @returns A function that takes the source code and arguments and returns the transformed code.
43+
*/
44+
export function createDirectiveTransformer(
45+
options: DirectiveTransformerOptions,
46+
) {
47+
const IMPORT_PATH = options.importPath;
48+
const DIRECTIVE = options.directive;
49+
const CACHE_IDENTIFIER = options.importName;
50+
const ASYNC_ONLY = options.asyncOnly ?? true;
51+
52+
const transformer = async (source: string, args: any) => {
53+
const ast = parser.parse(source, {
54+
sourceType: 'module',
55+
plugins: ['typescript', 'jsx'],
56+
});
57+
58+
let state = {
59+
needsImport: false,
60+
hasExistingImport: false,
61+
cacheIdentifierName: CACHE_IDENTIFIER,
62+
modifications: [],
63+
};
64+
65+
// First pass: check for naming collisions and collect modifications
66+
traverse(ast, {
67+
Program: {
68+
enter(path: any) {
69+
const binding = path.scope.getBinding(CACHE_IDENTIFIER);
70+
if (binding) {
71+
state.cacheIdentifierName = `${CACHE_IDENTIFIER}_${generateRandomString()}`;
72+
}
73+
},
74+
},
75+
76+
ImportDeclaration(path: any) {
77+
if (
78+
path.node.source.value === IMPORT_PATH &&
79+
path.node.specifiers.some(
80+
(spec: any) =>
81+
t.isImportSpecifier(spec) &&
82+
// @ts-ignore
83+
spec.imported.name === CACHE_IDENTIFIER,
84+
)
85+
) {
86+
state.hasExistingImport = true;
87+
if (state.cacheIdentifierName !== CACHE_IDENTIFIER) {
88+
// @ts-ignore
89+
state.modifications.push(() => {
90+
path.node.specifiers.forEach((spec: any) => {
91+
if (
92+
t.isImportSpecifier(spec) &&
93+
// @ts-ignore
94+
spec.imported.name === CACHE_IDENTIFIER
95+
) {
96+
spec.local.name = state.cacheIdentifierName;
97+
}
98+
});
99+
});
100+
}
101+
}
102+
},
103+
104+
'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ObjectMethod'(
105+
path: any,
106+
) {
107+
const body = t.isBlockStatement(path.node.body) ? path.node.body : null;
108+
const hasUseCache = body?.directives?.some(
109+
(d: any) => d.value.value === DIRECTIVE,
110+
);
111+
112+
if (!hasUseCache && !t.isBlockStatement(path.node.body)) {
113+
const parentFunction = path.findParent(
114+
(p: any) =>
115+
(p.isFunction() || p.isProgram()) && 'directives' in p.node,
116+
);
117+
if (
118+
!parentFunction?.node.directives?.some(
119+
(d: any) => d.value.value === DIRECTIVE,
120+
)
121+
) {
122+
return;
123+
}
124+
}
125+
126+
if (hasUseCache || !t.isBlockStatement(path.node.body)) {
127+
// Check if the function is async
128+
if (ASYNC_ONLY && !path.node.async) {
129+
throw new Error(
130+
`"${DIRECTIVE}" directive may only be used in async functions at ${args.path}\n\n${path.toString()}\n^^^^${'-'.repeat(6)} This function must be async`,
131+
);
132+
}
133+
134+
state.needsImport = true;
135+
const isDeclaration = t.isFunctionDeclaration(path.node);
136+
const name = isDeclaration ? path.node.id?.name : undefined;
137+
138+
// Create a new body without the 'use cache' directive
139+
const newBody = t.isBlockStatement(path.node.body)
140+
? t.blockStatement(
141+
path.node.body.body,
142+
path.node.body.directives.filter(
143+
(d: any) => d.value.value !== DIRECTIVE,
144+
),
145+
)
146+
: path.node.body;
147+
148+
const wrapped = t.callExpression(
149+
t.identifier(state.cacheIdentifierName),
150+
[t.arrowFunctionExpression(path.node.params, newBody, true)],
151+
);
152+
153+
// @ts-ignore
154+
state.modifications.push(() => {
155+
if (t.isObjectMethod(path.node)) {
156+
path.replaceWith(
157+
t.objectProperty(t.identifier(path.node.key.name), wrapped),
158+
);
159+
} else if (name) {
160+
path.replaceWith(
161+
t.variableDeclaration('const', [
162+
t.variableDeclarator(t.identifier(name), wrapped),
163+
]),
164+
);
165+
} else if (!t.isVariableDeclarator(path.parent)) {
166+
path.replaceWith(wrapped);
167+
} else {
168+
path.parent.init = wrapped;
169+
}
170+
});
171+
}
172+
},
173+
});
174+
175+
// Apply all collected modifications
176+
if (state.modifications.length > 0) {
177+
// Add import if needed
178+
if (state.needsImport && !state.hasExistingImport) {
179+
ast.program.body.unshift(
180+
t.importDeclaration(
181+
[
182+
t.importSpecifier(
183+
t.identifier(state.cacheIdentifierName),
184+
t.identifier(CACHE_IDENTIFIER),
185+
),
186+
],
187+
t.stringLiteral(IMPORT_PATH),
188+
),
189+
);
190+
}
191+
192+
// Apply collected modifications
193+
// @ts-ignore
194+
state.modifications.forEach((modify) => modify());
195+
}
196+
197+
const { code } = generate(ast);
198+
199+
return {
200+
contents: code,
201+
loader: args.path?.split('.').pop(),
202+
};
203+
};
204+
205+
return transformer;
206+
}

0 commit comments

Comments
 (0)