@kithinji/pod 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.js +22 -0
- package/dist/main.js +4464 -0
- package/dist/main.js.map +7 -0
- package/dist/types/add/component/component.d.ts +5 -0
- package/dist/types/add/component/component.d.ts.map +1 -0
- package/dist/types/add/component/index.d.ts +2 -0
- package/dist/types/add/component/index.d.ts.map +1 -0
- package/dist/types/add/index.d.ts +4 -0
- package/dist/types/add/index.d.ts.map +1 -0
- package/dist/types/add/module/index.d.ts +2 -0
- package/dist/types/add/module/index.d.ts.map +1 -0
- package/dist/types/add/module/module.d.ts +3 -0
- package/dist/types/add/module/module.d.ts.map +1 -0
- package/dist/types/add/new/index.d.ts +2 -0
- package/dist/types/add/new/index.d.ts.map +1 -0
- package/dist/types/config/config.d.ts +18 -0
- package/dist/types/config/config.d.ts.map +1 -0
- package/dist/types/config/index.d.ts +2 -0
- package/dist/types/config/index.d.ts.map +1 -0
- package/dist/types/dev/index.d.ts +2 -0
- package/dist/types/dev/index.d.ts.map +1 -0
- package/dist/types/dev/project.d.ts +9 -0
- package/dist/types/dev/project.d.ts.map +1 -0
- package/dist/types/dev/server.d.ts +2 -0
- package/dist/types/dev/server.d.ts.map +1 -0
- package/dist/types/docker/docker.d.ts +2 -0
- package/dist/types/docker/docker.d.ts.map +1 -0
- package/dist/types/docker/index.d.ts +2 -0
- package/dist/types/docker/index.d.ts.map +1 -0
- package/dist/types/macros/expand_macros.d.ts +48 -0
- package/dist/types/macros/expand_macros.d.ts.map +1 -0
- package/dist/types/macros/index.d.ts +3 -0
- package/dist/types/macros/index.d.ts.map +1 -0
- package/dist/types/macros/macro_executer.d.ts +12 -0
- package/dist/types/macros/macro_executer.d.ts.map +1 -0
- package/dist/types/main.d.ts +13 -0
- package/dist/types/main.d.ts.map +1 -0
- package/dist/types/plugins/analyzers/graph.d.ts +25 -0
- package/dist/types/plugins/analyzers/graph.d.ts.map +1 -0
- package/dist/types/plugins/css/index.d.ts +7 -0
- package/dist/types/plugins/css/index.d.ts.map +1 -0
- package/dist/types/plugins/generators/generate_controller.d.ts +2 -0
- package/dist/types/plugins/generators/generate_controller.d.ts.map +1 -0
- package/dist/types/plugins/generators/generate_rsc.d.ts +2 -0
- package/dist/types/plugins/generators/generate_rsc.d.ts.map +1 -0
- package/dist/types/plugins/generators/generate_server_component.d.ts +2 -0
- package/dist/types/plugins/generators/generate_server_component.d.ts.map +1 -0
- package/dist/types/plugins/generators/tsx_server_stub.d.ts +2 -0
- package/dist/types/plugins/generators/tsx_server_stub.d.ts.map +1 -0
- package/dist/types/plugins/index.d.ts +4 -0
- package/dist/types/plugins/index.d.ts.map +1 -0
- package/dist/types/plugins/my.d.ts +10 -0
- package/dist/types/plugins/my.d.ts.map +1 -0
- package/dist/types/plugins/transformers/j2d.d.ts +11 -0
- package/dist/types/plugins/transformers/j2d.d.ts.map +1 -0
- package/dist/types/store/index.d.ts +2 -0
- package/dist/types/store/index.d.ts.map +1 -0
- package/dist/types/store/store.d.ts +14 -0
- package/dist/types/store/store.d.ts.map +1 -0
- package/dist/types/utils/cases.d.ts +4 -0
- package/dist/types/utils/cases.d.ts.map +1 -0
- package/dist/types/utils/create.d.ts +12 -0
- package/dist/types/utils/create.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +3 -0
- package/dist/types/utils/index.d.ts.map +1 -0
- package/package.json +44 -0
- package/src/add/component/component.ts +496 -0
- package/src/add/component/index.ts +1 -0
- package/src/add/index.ts +3 -0
- package/src/add/module/index.ts +1 -0
- package/src/add/module/module.ts +521 -0
- package/src/add/new/index.ts +135 -0
- package/src/config/config.ts +141 -0
- package/src/config/index.ts +1 -0
- package/src/dev/index.ts +1 -0
- package/src/dev/project.ts +45 -0
- package/src/dev/server.ts +190 -0
- package/src/docker/docker.ts +452 -0
- package/src/docker/index.ts +1 -0
- package/src/macros/expand_macros.ts +791 -0
- package/src/macros/index.ts +2 -0
- package/src/macros/macro_executer.ts +189 -0
- package/src/main.ts +95 -0
- package/src/plugins/analyzers/graph.ts +291 -0
- package/src/plugins/css/index.ts +25 -0
- package/src/plugins/generators/generate_controller.ts +308 -0
- package/src/plugins/generators/generate_rsc.ts +274 -0
- package/src/plugins/generators/generate_server_component.ts +279 -0
- package/src/plugins/generators/tsx_server_stub.ts +295 -0
- package/src/plugins/index.ts +3 -0
- package/src/plugins/my.ts +274 -0
- package/src/plugins/transformers/j2d.ts +1014 -0
- package/src/store/index.ts +1 -0
- package/src/store/store.ts +44 -0
- package/src/utils/cases.ts +15 -0
- package/src/utils/create.ts +26 -0
- package/src/utils/index.ts +2 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { parseSync } from "@swc/core";
|
|
3
|
+
import type {
|
|
4
|
+
ClassDeclaration,
|
|
5
|
+
Decorator,
|
|
6
|
+
Param,
|
|
7
|
+
ImportDeclaration,
|
|
8
|
+
} from "@swc/core";
|
|
9
|
+
|
|
10
|
+
interface ServiceMethod {
|
|
11
|
+
name: string;
|
|
12
|
+
params: MethodParam[];
|
|
13
|
+
returnType: string;
|
|
14
|
+
isAsync: boolean;
|
|
15
|
+
paramSchemas: string[];
|
|
16
|
+
returnSchema?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface MethodParam {
|
|
20
|
+
name: string;
|
|
21
|
+
type: string;
|
|
22
|
+
decorators: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ServiceInfo {
|
|
26
|
+
className: string;
|
|
27
|
+
methods: ServiceMethod[];
|
|
28
|
+
hasInjectable: boolean;
|
|
29
|
+
importMap: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function generateController(
|
|
33
|
+
filePath: string,
|
|
34
|
+
code: string
|
|
35
|
+
): string | null {
|
|
36
|
+
const ast = parseSync(code, {
|
|
37
|
+
syntax: "typescript",
|
|
38
|
+
tsx: filePath.endsWith("x"),
|
|
39
|
+
decorators: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const serviceInfo = extractServiceInfo(ast);
|
|
43
|
+
if (!serviceInfo || !serviceInfo.hasInjectable) return null;
|
|
44
|
+
|
|
45
|
+
return generateControllerCode(serviceInfo, filePath);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractServiceInfo(ast: any): ServiceInfo | null {
|
|
49
|
+
let serviceClass: ClassDeclaration | null = null;
|
|
50
|
+
let hasInjectable = false;
|
|
51
|
+
const importMap: Record<string, string> = {};
|
|
52
|
+
|
|
53
|
+
for (const item of ast.body) {
|
|
54
|
+
if (item.type === "ImportDeclaration") {
|
|
55
|
+
const decl = item as ImportDeclaration;
|
|
56
|
+
const source = decl.source.value;
|
|
57
|
+
decl.specifiers.forEach((spec) => {
|
|
58
|
+
if (
|
|
59
|
+
spec.type === "ImportSpecifier" ||
|
|
60
|
+
spec.type === "ImportDefaultSpecifier" ||
|
|
61
|
+
spec.type === "ImportNamespaceSpecifier"
|
|
62
|
+
) {
|
|
63
|
+
importMap[spec.local.value] = source;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
item.type === "ExportDeclaration" &&
|
|
70
|
+
item.declaration.type === "ClassDeclaration"
|
|
71
|
+
) {
|
|
72
|
+
const classDecl = item.declaration as ClassDeclaration;
|
|
73
|
+
if (hasInjectableDecorator(classDecl.decorators)) {
|
|
74
|
+
serviceClass = classDecl;
|
|
75
|
+
hasInjectable = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!serviceClass || !serviceClass.identifier) return null;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
className: serviceClass.identifier.value,
|
|
84
|
+
methods: extractMethods(serviceClass),
|
|
85
|
+
hasInjectable,
|
|
86
|
+
importMap,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasInjectableDecorator(decorators?: Decorator[]): boolean {
|
|
91
|
+
return (
|
|
92
|
+
decorators?.some((d) => {
|
|
93
|
+
const expr = d.expression;
|
|
94
|
+
return (
|
|
95
|
+
(expr.type === "Identifier" && expr.value === "Injectable") ||
|
|
96
|
+
(expr.type === "CallExpression" &&
|
|
97
|
+
expr.callee.type === "Identifier" &&
|
|
98
|
+
expr.callee.value === "Injectable")
|
|
99
|
+
);
|
|
100
|
+
}) ?? false
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function extractMethods(classDecl: ClassDeclaration): ServiceMethod[] {
|
|
105
|
+
const methods: ServiceMethod[] = [];
|
|
106
|
+
|
|
107
|
+
for (const member of classDecl.body) {
|
|
108
|
+
if (member.type === "ClassMethod" && member.accessibility === "public") {
|
|
109
|
+
const method = member as any;
|
|
110
|
+
const methodName =
|
|
111
|
+
method.key.type === "Identifier" ? method.key.value : "";
|
|
112
|
+
if (!methodName) continue;
|
|
113
|
+
|
|
114
|
+
if (!method.function.async) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Server action ${classDecl.identifier.value}.${methodName} must be async.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { paramSchemas, returnSchema } = extractSignature(
|
|
121
|
+
method.function.decorators,
|
|
122
|
+
method.function.params.length
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
methods.push({
|
|
126
|
+
name: methodName,
|
|
127
|
+
params: extractMethodParams(method.function.params),
|
|
128
|
+
returnType: extractReturnType(method.function.returnType),
|
|
129
|
+
isAsync: true,
|
|
130
|
+
paramSchemas,
|
|
131
|
+
returnSchema,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return methods;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractSignature(
|
|
139
|
+
decorators: Decorator[] | undefined,
|
|
140
|
+
paramCount: number
|
|
141
|
+
) {
|
|
142
|
+
if (!decorators) return { paramSchemas: [] };
|
|
143
|
+
|
|
144
|
+
for (const decorator of decorators) {
|
|
145
|
+
const expr = decorator.expression;
|
|
146
|
+
if (
|
|
147
|
+
expr.type === "CallExpression" &&
|
|
148
|
+
expr.callee.type === "Identifier" &&
|
|
149
|
+
expr.callee.value === "Signature"
|
|
150
|
+
) {
|
|
151
|
+
const args = expr.arguments;
|
|
152
|
+
if (args.length === 0) return { paramSchemas: [] };
|
|
153
|
+
|
|
154
|
+
const schemaStrings = args.map((arg) =>
|
|
155
|
+
stringifyExpression(arg.expression)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (args.length === 1) {
|
|
159
|
+
return { paramSchemas: [], returnSchema: schemaStrings[0] };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
paramSchemas: schemaStrings.slice(0, -1),
|
|
164
|
+
returnSchema: schemaStrings[schemaStrings.length - 1],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { paramSchemas: [] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function stringifyExpression(expr: any): string {
|
|
172
|
+
if (expr.type === "Identifier") return expr.value;
|
|
173
|
+
if (expr.type === "MemberExpression") {
|
|
174
|
+
return `${stringifyExpression(expr.object)}.${expr.property.value || ""}`;
|
|
175
|
+
}
|
|
176
|
+
if (expr.type === "CallExpression") {
|
|
177
|
+
const args = expr.arguments
|
|
178
|
+
.map((a: any) => stringifyExpression(a.expression))
|
|
179
|
+
.join(", ");
|
|
180
|
+
return `${stringifyExpression(expr.callee)}(${args})`;
|
|
181
|
+
}
|
|
182
|
+
return "any";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function extractMethodParams(params: Param[]): MethodParam[] {
|
|
186
|
+
return params.map((p) => {
|
|
187
|
+
const pat = (p as any).pat;
|
|
188
|
+
return {
|
|
189
|
+
name: pat.value,
|
|
190
|
+
type: pat.typeAnnotation
|
|
191
|
+
? stringifyType(pat.typeAnnotation.typeAnnotation)
|
|
192
|
+
: "any",
|
|
193
|
+
decorators: [],
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function extractReturnType(node: any): string {
|
|
199
|
+
if (!node?.typeAnnotation) return "any";
|
|
200
|
+
const type = node.typeAnnotation;
|
|
201
|
+
if (type.type === "TsTypeReference" && type.typeName.value === "Promise") {
|
|
202
|
+
return stringifyType(type.typeParams?.params[0]);
|
|
203
|
+
}
|
|
204
|
+
return stringifyType(type);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function stringifyType(node: any): string {
|
|
208
|
+
if (!node) return "any";
|
|
209
|
+
switch (node.type) {
|
|
210
|
+
case "TsKeywordType":
|
|
211
|
+
return node.kind;
|
|
212
|
+
case "TsTypeReference":
|
|
213
|
+
const base = node.typeName.value;
|
|
214
|
+
const args = node.typeParams
|
|
215
|
+
? `<${node.typeParams.params.map(stringifyType).join(", ")}>`
|
|
216
|
+
: "";
|
|
217
|
+
return base + args;
|
|
218
|
+
case "TsArrayType":
|
|
219
|
+
return `${stringifyType(node.elemType)}[]`;
|
|
220
|
+
default:
|
|
221
|
+
return "any";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function generateControllerCode(
|
|
226
|
+
serviceInfo: ServiceInfo,
|
|
227
|
+
filePath: string
|
|
228
|
+
): string {
|
|
229
|
+
const serviceName = serviceInfo.className;
|
|
230
|
+
const controllerName = serviceName.replace(/Service$/, "AutoController");
|
|
231
|
+
const serviceImportPath = `./${path.basename(filePath).replace(/\.ts$/, "")}`;
|
|
232
|
+
|
|
233
|
+
const importGroups = new Map<string, Set<string>>();
|
|
234
|
+
|
|
235
|
+
const registerIdentifier = (id: string) => {
|
|
236
|
+
const source = serviceInfo.importMap[id] || serviceImportPath;
|
|
237
|
+
if (!importGroups.has(source)) importGroups.set(source, new Set());
|
|
238
|
+
importGroups.get(source)!.add(id);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
serviceInfo.methods.forEach((m) => {
|
|
242
|
+
[...m.paramSchemas, m.returnSchema].filter(Boolean).forEach((s) => {
|
|
243
|
+
const matches = s!.match(/[A-Z][a-zA-Z0-9]*/g);
|
|
244
|
+
matches?.forEach(registerIdentifier);
|
|
245
|
+
if (s!.includes("z.")) registerIdentifier("z");
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
let importStrings = `import { Controller, Post, Get, Body } from "@kithinji/orca";\n`;
|
|
250
|
+
|
|
251
|
+
importGroups.forEach((ids, source) => {
|
|
252
|
+
const filteredIds = Array.from(ids).filter((id) => id !== serviceName);
|
|
253
|
+
if (filteredIds.length > 0) {
|
|
254
|
+
importStrings += `\nimport { ${filteredIds.join(
|
|
255
|
+
", "
|
|
256
|
+
)} } from "${source}";`;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const methods = serviceInfo.methods
|
|
261
|
+
.map((m) => {
|
|
262
|
+
const hasParams = m.params.length > 0;
|
|
263
|
+
const bodyParam = hasParams ? `@Body() body: any` : "";
|
|
264
|
+
|
|
265
|
+
let body = "";
|
|
266
|
+
if (hasParams) {
|
|
267
|
+
if (m.paramSchemas.length > 0) {
|
|
268
|
+
body += ` const b = typeof body === 'object' && body !== null ? body : {};\n`;
|
|
269
|
+
m.params.forEach((p, i) => {
|
|
270
|
+
body += ` const ${p.name} = ${m.paramSchemas[i]}.parse(b.${p.name});\n`;
|
|
271
|
+
});
|
|
272
|
+
} else {
|
|
273
|
+
body += ` const { ${m.params
|
|
274
|
+
.map((p) => p.name)
|
|
275
|
+
.join(", ")} } = body;\n`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const callArgs = m.params.map((p) => p.name).join(", ");
|
|
280
|
+
const serviceCall = `this.${
|
|
281
|
+
serviceName.charAt(0).toLowerCase() + serviceName.slice(1)
|
|
282
|
+
}.${m.name}(${callArgs})`;
|
|
283
|
+
|
|
284
|
+
if (m.returnSchema) {
|
|
285
|
+
body += ` const res = await ${serviceCall};\n return ${m.returnSchema}.parse(res);`;
|
|
286
|
+
} else {
|
|
287
|
+
body += ` return ${serviceCall};`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return ` @${hasParams ? "Post" : "Get"}("${m.name}")\n async ${
|
|
291
|
+
m.name
|
|
292
|
+
}(${bodyParam}): Promise<${m.returnType}> {\n${body}\n }`;
|
|
293
|
+
})
|
|
294
|
+
.join("\n\n");
|
|
295
|
+
|
|
296
|
+
return `${importStrings}
|
|
297
|
+
|
|
298
|
+
@Controller("/${serviceName}", {
|
|
299
|
+
providedIn: "root",
|
|
300
|
+
})
|
|
301
|
+
export class ${controllerName} {
|
|
302
|
+
constructor(private readonly ${
|
|
303
|
+
serviceName.charAt(0).toLowerCase() + serviceName.slice(1)
|
|
304
|
+
}: ${serviceName}) {}
|
|
305
|
+
|
|
306
|
+
${methods}
|
|
307
|
+
}`;
|
|
308
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { parseSync } from "@swc/core";
|
|
2
|
+
import type { ClassDeclaration, Decorator } from "@swc/core";
|
|
3
|
+
|
|
4
|
+
interface ServiceMethod {
|
|
5
|
+
name: string;
|
|
6
|
+
params: MethodParam[];
|
|
7
|
+
returnType: string;
|
|
8
|
+
isAsync: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface MethodParam {
|
|
12
|
+
name: string;
|
|
13
|
+
type: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ServiceInfo {
|
|
17
|
+
className: string;
|
|
18
|
+
methods: ServiceMethod[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function generateRscStub(filePath: string, code: string): string {
|
|
22
|
+
const ast = parseSync(code, {
|
|
23
|
+
syntax: "typescript",
|
|
24
|
+
tsx: filePath.endsWith("x"),
|
|
25
|
+
decorators: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const serviceInfo = extractServiceInfo(ast);
|
|
29
|
+
|
|
30
|
+
return generateStubCode(serviceInfo);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractServiceInfo(ast: any): ServiceInfo {
|
|
34
|
+
let serviceClass: ClassDeclaration | null = null;
|
|
35
|
+
|
|
36
|
+
for (const item of ast.body) {
|
|
37
|
+
if (
|
|
38
|
+
item.type === "ExportDeclaration" &&
|
|
39
|
+
item.declaration.type === "ClassDeclaration"
|
|
40
|
+
) {
|
|
41
|
+
const classDecl = item.declaration as ClassDeclaration;
|
|
42
|
+
|
|
43
|
+
if (hasInjectableDecorator(classDecl.decorators)) {
|
|
44
|
+
serviceClass = classDecl;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!serviceClass || !serviceClass.identifier) {
|
|
51
|
+
throw new Error("Service class is undefined");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const className = serviceClass.identifier.value;
|
|
55
|
+
const methods = extractMethods(serviceClass);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
className,
|
|
59
|
+
methods,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasInjectableDecorator(decorators?: Decorator[]): boolean {
|
|
64
|
+
if (!decorators) return false;
|
|
65
|
+
|
|
66
|
+
return decorators.some((decorator) => {
|
|
67
|
+
const expr = decorator.expression;
|
|
68
|
+
|
|
69
|
+
if (expr.type === "CallExpression") {
|
|
70
|
+
if (
|
|
71
|
+
expr.callee.type === "Identifier" &&
|
|
72
|
+
expr.callee.value === "Injectable"
|
|
73
|
+
) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (expr.type === "Identifier" && expr.value === "Injectable") {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractMethods(classDecl: ClassDeclaration): ServiceMethod[] {
|
|
87
|
+
const methods: ServiceMethod[] = [];
|
|
88
|
+
|
|
89
|
+
for (const member of classDecl.body) {
|
|
90
|
+
if (member.type === "ClassMethod" && member.accessibility === "public") {
|
|
91
|
+
const method = member as any;
|
|
92
|
+
|
|
93
|
+
const methodName =
|
|
94
|
+
method.key.type === "Identifier" ? method.key.value : "";
|
|
95
|
+
|
|
96
|
+
if (!methodName) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!method.function.async) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Server action ${classDecl.identifier.value}.${methodName} must be async.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const params = extractMethodParams(method.function.params || []);
|
|
107
|
+
const returnType = extractReturnType(method.function.returnType);
|
|
108
|
+
const isAsync = method.function.async || false;
|
|
109
|
+
|
|
110
|
+
methods.push({
|
|
111
|
+
name: methodName,
|
|
112
|
+
params,
|
|
113
|
+
returnType,
|
|
114
|
+
isAsync,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return methods;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function extractMethodParams(params: any[]): MethodParam[] {
|
|
123
|
+
const result: MethodParam[] = [];
|
|
124
|
+
|
|
125
|
+
for (const param of params) {
|
|
126
|
+
if (param.type === "Parameter") {
|
|
127
|
+
const pat = param.pat as any;
|
|
128
|
+
|
|
129
|
+
if (pat.type === "Identifier") {
|
|
130
|
+
const name = pat.value;
|
|
131
|
+
const type = pat.typeAnnotation?.typeAnnotation
|
|
132
|
+
? stringifyType(pat.typeAnnotation.typeAnnotation)
|
|
133
|
+
: "any";
|
|
134
|
+
|
|
135
|
+
result.push({
|
|
136
|
+
name,
|
|
137
|
+
type,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function extractReturnType(returnType?: any): string {
|
|
147
|
+
if (!returnType || !returnType.typeAnnotation) {
|
|
148
|
+
return "any";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const type = returnType.typeAnnotation;
|
|
152
|
+
|
|
153
|
+
if (type.type === "TsTypeReference") {
|
|
154
|
+
const typeName = type.typeName;
|
|
155
|
+
if (typeName.type === "Identifier" && typeName.value === "Promise") {
|
|
156
|
+
if (type.typeParams && type.typeParams.params.length > 0) {
|
|
157
|
+
return stringifyType(type.typeParams.params[0]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return stringifyType(type);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function stringifyType(typeNode: any): string {
|
|
166
|
+
if (!typeNode) return "any";
|
|
167
|
+
|
|
168
|
+
switch (typeNode.type) {
|
|
169
|
+
case "TsKeywordType":
|
|
170
|
+
return typeNode.kind;
|
|
171
|
+
|
|
172
|
+
case "TsTypeReference":
|
|
173
|
+
if (typeNode.typeName.type === "Identifier") {
|
|
174
|
+
const baseName = typeNode.typeName.value;
|
|
175
|
+
if (typeNode.typeParams && typeNode.typeParams.params.length > 0) {
|
|
176
|
+
const params = typeNode.typeParams.params
|
|
177
|
+
.map(stringifyType)
|
|
178
|
+
.join(", ");
|
|
179
|
+
return `${baseName}<${params}>`;
|
|
180
|
+
}
|
|
181
|
+
return baseName;
|
|
182
|
+
}
|
|
183
|
+
return "any";
|
|
184
|
+
|
|
185
|
+
case "TsArrayType":
|
|
186
|
+
return `${stringifyType(typeNode.elemType)}[]`;
|
|
187
|
+
|
|
188
|
+
case "TsUnionType":
|
|
189
|
+
return typeNode.types.map(stringifyType).join(" | ");
|
|
190
|
+
|
|
191
|
+
case "TsIntersectionType":
|
|
192
|
+
return typeNode.types.map(stringifyType).join(" & ");
|
|
193
|
+
|
|
194
|
+
case "TsTypeLiteral":
|
|
195
|
+
const props = typeNode.members
|
|
196
|
+
.map((member: any) => {
|
|
197
|
+
if (member.type === "TsPropertySignature") {
|
|
198
|
+
const key =
|
|
199
|
+
member.key.type === "Identifier" ? member.key.value : "";
|
|
200
|
+
const type = member.typeAnnotation
|
|
201
|
+
? stringifyType(member.typeAnnotation.typeAnnotation)
|
|
202
|
+
: "any";
|
|
203
|
+
return `${key}: ${type}`;
|
|
204
|
+
}
|
|
205
|
+
return "";
|
|
206
|
+
})
|
|
207
|
+
.filter(Boolean);
|
|
208
|
+
return `{ ${props.join("; ")} }`;
|
|
209
|
+
|
|
210
|
+
default:
|
|
211
|
+
return "any";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function generateStubCode(serviceInfo: ServiceInfo): string {
|
|
216
|
+
const className = serviceInfo.className;
|
|
217
|
+
|
|
218
|
+
const methods = serviceInfo.methods
|
|
219
|
+
.map((method) => {
|
|
220
|
+
const params = method.params
|
|
221
|
+
.map((p) => `${p.name}: ${p.type}`)
|
|
222
|
+
.join(", ");
|
|
223
|
+
const paramNames = method.params.map((p) => p.name).join(", ");
|
|
224
|
+
|
|
225
|
+
const asyncKeyword = method.isAsync ? "async " : "";
|
|
226
|
+
const returnType = method.isAsync
|
|
227
|
+
? `Promise<${method.returnType}>`
|
|
228
|
+
: method.returnType;
|
|
229
|
+
|
|
230
|
+
const hasParams = method.params.length > 0;
|
|
231
|
+
const bodyParam = hasParams ? `{ ${paramNames} }` : "{}";
|
|
232
|
+
|
|
233
|
+
if (!hasParams) {
|
|
234
|
+
return ` ${asyncKeyword}${method.name}(${params}): ${returnType} {
|
|
235
|
+
const response = await fetch(\`/${className}/${method.name}\`, {
|
|
236
|
+
method: 'GET',
|
|
237
|
+
headers: {
|
|
238
|
+
'Content-Type': 'application/json',
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
throw new Error(\`HTTP error! status: \${response.status}\`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return response.json();
|
|
247
|
+
}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return ` ${asyncKeyword}${method.name}(${params}): ${returnType} {
|
|
251
|
+
const response = await fetch(\`/${className}/${method.name}\`, {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: {
|
|
254
|
+
'Content-Type': 'application/json',
|
|
255
|
+
},
|
|
256
|
+
body: JSON.stringify(${bodyParam}),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
throw new Error(\`HTTP error! status: \${response.status}\`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return response.json();
|
|
264
|
+
}`;
|
|
265
|
+
})
|
|
266
|
+
.join("\n\n");
|
|
267
|
+
|
|
268
|
+
return `import { Injectable } from "@kithinji/orca";
|
|
269
|
+
|
|
270
|
+
@Injectable()
|
|
271
|
+
export class ${className} {
|
|
272
|
+
${methods}
|
|
273
|
+
}`;
|
|
274
|
+
}
|