milkio 0.0.1
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/.co.toml +2 -0
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/c.ts +83 -0
- package/defines/define-api-test-handler.ts +70 -0
- package/defines/define-api-test.ts +14 -0
- package/defines/define-api.ts +15 -0
- package/defines/define-http-handler.ts +203 -0
- package/defines/define-middleware.ts +9 -0
- package/defines/define-use.ts +13 -0
- package/index.ts +33 -0
- package/kernel/config.ts +15 -0
- package/kernel/context.ts +24 -0
- package/kernel/fail.ts +22 -0
- package/kernel/logger.ts +113 -0
- package/kernel/loongbao.ts +269 -0
- package/kernel/meta.ts +9 -0
- package/kernel/middleware.ts +50 -0
- package/kernel/runtime.ts +16 -0
- package/kernel/validate.ts +13 -0
- package/package.json +24 -0
- package/scripts/build-cookbook.ts +233 -0
- package/scripts/build-dto.ts +65 -0
- package/scripts/generate/generate-app-partial.ts +74 -0
- package/scripts/generate/generate-app.ts +153 -0
- package/scripts/generate/generate-database.ts +23 -0
- package/scripts/generate-database.ts +23 -0
- package/scripts/generate-partial.ts +15 -0
- package/scripts/generate.ts +23 -0
- package/templates/api.ts +49 -0
- package/tsconfig.json +33 -0
- package/types.ts +42 -0
- package/utils/create-template.ts +32 -0
- package/utils/create-ulid.ts +5 -0
- package/utils/env-to-boolean.ts +11 -0
- package/utils/env-to-number.ts +5 -0
- package/utils/env-to-string.ts +5 -0
- package/utils/exec.ts +27 -0
- package/utils/handle-catch-error.ts +37 -0
- package/utils/remove-dir.ts +22 -0
- package/utils/tson.ts +3 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/* eslint-disable no-console, @typescript-eslint/no-invalid-void-type, @typescript-eslint/await-thenable, @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { type MiddlewareOptions, _middlewares, MiddlewareEvent } from "./middleware";
|
|
3
|
+
import schema from "../../../generate/api-schema";
|
|
4
|
+
import type { Context } from "../../../src/context";
|
|
5
|
+
import { failCode } from "../../../src/fail-code";
|
|
6
|
+
import type { FrameworkContext } from "./context";
|
|
7
|
+
import { type Mixin, type ExecuteId, type Fail, type FailEnumerates, loggerPushTags, loggerSubmit, runtime, TSON, type Logger, useLogger } from "..";
|
|
8
|
+
import { hanldeCatchError } from "../utils/handle-catch-error";
|
|
9
|
+
import { _validate } from "./validate";
|
|
10
|
+
import { cwd, exit } from "node:process";
|
|
11
|
+
import { createUlid } from "../utils/create-ulid";
|
|
12
|
+
|
|
13
|
+
export type LoongbaoAppOptions = {
|
|
14
|
+
/**
|
|
15
|
+
* bootstraps
|
|
16
|
+
* @description
|
|
17
|
+
* When Loongbao is launched, all methods in this array will run **in parallel**.
|
|
18
|
+
*/
|
|
19
|
+
bootstraps?: () => Array</* This type is long, and its intention is to prevent someone from forgetting to add parentheses when adding bootstraps. Therefore, it allows all types except methods */ Promise<unknown> | void | string | number | boolean | null | undefined | Record<string | number | symbol, unknown> | Array<unknown>>;
|
|
20
|
+
/**
|
|
21
|
+
* middlewares
|
|
22
|
+
* @description
|
|
23
|
+
* When Loongbao is launched, the closer it is to the front of the array, the more it is on the outer layer of the "onion".
|
|
24
|
+
*/
|
|
25
|
+
middlewares?: () => Array<MiddlewareOptions>;
|
|
26
|
+
/**
|
|
27
|
+
* maxRequest
|
|
28
|
+
* @description
|
|
29
|
+
* When the function runs for a long time, it is possible that the memory will continuously expand (not necessarily due to memory leaks, but also possibly due to having a large number of routes).
|
|
30
|
+
* Set a maximum number of requests, when the number of requests reaches this value, kill the process and automatically restart it from outside (K8S or whatever).
|
|
31
|
+
*/
|
|
32
|
+
enableMaxRequestLimit?: number | null | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* maxRunningTime (minutes)
|
|
35
|
+
* @description
|
|
36
|
+
* When the function runs for a long time, it is possible that the memory will continuously expand (not necessarily due to memory leaks, but also possibly due to having a large number of routes).
|
|
37
|
+
* Set the maximum running time (in minutes). When Loongbao's running time reaches this value, terminate the process and automatically restart it from outside (K8S or other means).
|
|
38
|
+
*/
|
|
39
|
+
enableMaxRunningTimeoutLimit?: number | null | undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export async function createLoongbaoApp(loongbaoAppOptions: LoongbaoAppOptions = {}) {
|
|
43
|
+
if (loongbaoAppOptions?.enableMaxRequestLimit && loongbaoAppOptions.enableMaxRequestLimit >= 1) {
|
|
44
|
+
runtime.maxRequest.expected = loongbaoAppOptions.enableMaxRequestLimit;
|
|
45
|
+
runtime.maxRequest.enable = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (loongbaoAppOptions.enableMaxRunningTimeoutLimit && loongbaoAppOptions.enableMaxRunningTimeoutLimit >= 1) {
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
console.log('❌ Loongbao reached the limit of "maxRunningTimeout" in the options and automatically exited.');
|
|
51
|
+
exit(0);
|
|
52
|
+
}, loongbaoAppOptions.enableMaxRunningTimeoutLimit * 60 * 1000);
|
|
53
|
+
runtime.maxRunningTimeout.enable = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const loongbaoApp = {
|
|
57
|
+
execute: _execute,
|
|
58
|
+
executeToJson: _executeToJson,
|
|
59
|
+
_executeCore,
|
|
60
|
+
_executeCoreToJson
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (loongbaoAppOptions.bootstraps) {
|
|
64
|
+
await Promise.all(loongbaoAppOptions.bootstraps());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (loongbaoAppOptions.middlewares) {
|
|
68
|
+
MiddlewareEvent.define("bootstrap", (a, b) => a.index - b.index);
|
|
69
|
+
MiddlewareEvent.define("beforeExecute", (a, b) => a.index - b.index);
|
|
70
|
+
MiddlewareEvent.define("afterExecute", (a, b) => b.index - a.index);
|
|
71
|
+
MiddlewareEvent.define("afterHTTPRequest", (a, b) => a.index - b.index);
|
|
72
|
+
MiddlewareEvent.define("beforeHTTPResponse", (a, b) => b.index - a.index);
|
|
73
|
+
|
|
74
|
+
const middlewares = loongbaoAppOptions.middlewares();
|
|
75
|
+
|
|
76
|
+
for (let index = 0; index < middlewares.length; index++) {
|
|
77
|
+
const middlewareOptions = middlewares[index];
|
|
78
|
+
for (const name in middlewareOptions) {
|
|
79
|
+
let middleware = _middlewares.get(name);
|
|
80
|
+
if (middleware === undefined) {
|
|
81
|
+
middleware = [];
|
|
82
|
+
_middlewares.set(name, middleware);
|
|
83
|
+
}
|
|
84
|
+
const id = createUlid();
|
|
85
|
+
middleware.push({ id, index, middleware: middlewareOptions[name] });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
MiddlewareEvent._sort();
|
|
89
|
+
|
|
90
|
+
await MiddlewareEvent.handle("bootstrap", [loongbaoApp]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`🧊 Loongbao is running on : ${cwd()}`);
|
|
94
|
+
|
|
95
|
+
return loongbaoApp;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function _execute<Path extends keyof (typeof schema)["apiMethodsTypeSchema"], Result extends Awaited<ReturnType<(typeof schema)["apiMethodsTypeSchema"][Path]["api"]["action"]>>>(path: Path, params: Parameters<(typeof schema)["apiMethodsTypeSchema"][Path]["api"]["action"]>[0] | string, headersInit: Record<string, string> | Headers = {}, options?: ExecuteOptions): Promise<ExecuteResult<Result>> {
|
|
99
|
+
const executeId = (options?.executeId ?? createUlid()) as ExecuteId;
|
|
100
|
+
const logger = useLogger(executeId);
|
|
101
|
+
runtime.execute.executeIds.add(executeId);
|
|
102
|
+
|
|
103
|
+
loggerPushTags(executeId, {
|
|
104
|
+
from: "execute",
|
|
105
|
+
executeId,
|
|
106
|
+
params,
|
|
107
|
+
path
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result: any = await _executeCore(path, params, headersInit, {
|
|
111
|
+
...options,
|
|
112
|
+
executeId,
|
|
113
|
+
logger,
|
|
114
|
+
onAfterHeaders: (headers) => {
|
|
115
|
+
loggerPushTags(executeId, {
|
|
116
|
+
headers: headers.toJSON()
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
loggerPushTags(executeId, { result });
|
|
122
|
+
await loggerSubmit(executeId);
|
|
123
|
+
runtime.execute.executeIds.delete(executeId);
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* executeCore is a low-level API that is useful only when you want to execute the loongbao API without using execute or httpServer.
|
|
130
|
+
* It only does the most basic thing internally, which is calling the API. The external handling of functions such as making executeId, logging, middleware, etc., are all handled externally.
|
|
131
|
+
* Both execute and httpServer essentially call executeCore.
|
|
132
|
+
*/
|
|
133
|
+
async function _executeCore<Path extends keyof (typeof schema)["apiMethodsTypeSchema"], Result extends Awaited<ReturnType<(typeof schema)["apiMethodsTypeSchema"][Path]["api"]["action"]>>>(path: Path, params: Parameters<(typeof schema)["apiMethodsTypeSchema"][Path]["api"]["action"]>[0] | string, headersInit: Record<string, string> | Headers = {}, options: ExecuteCoreOptions): Promise<ExecuteResult<Result>> {
|
|
134
|
+
const executeId = options.executeId as ExecuteId;
|
|
135
|
+
|
|
136
|
+
params = TSON.decode(params);
|
|
137
|
+
|
|
138
|
+
if (runtime.maxRequest.enable) {
|
|
139
|
+
if (runtime.maxRequest.counter >= runtime.maxRequest.expected) {
|
|
140
|
+
console.log("❌ Loongbao reached the limit of 'maxRequest' in the options and automatically exited.");
|
|
141
|
+
exit(0);
|
|
142
|
+
}
|
|
143
|
+
runtime.maxRequest.counter++;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!(path in schema.apiMethodsSchema)) {
|
|
147
|
+
const result = {
|
|
148
|
+
executeId,
|
|
149
|
+
success: false,
|
|
150
|
+
fail: {
|
|
151
|
+
code: "not-found",
|
|
152
|
+
message: failCode["not-found"](),
|
|
153
|
+
data: undefined
|
|
154
|
+
}
|
|
155
|
+
} satisfies ExecuteResult<Result>;
|
|
156
|
+
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let headers: Headers;
|
|
161
|
+
if (!(headersInit instanceof Headers)) {
|
|
162
|
+
headers = new Headers({
|
|
163
|
+
...headersInit
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
headers = headersInit;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (options?.onAfterHeaders) {
|
|
170
|
+
await options.onAfterHeaders(headers);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const context: Context = {
|
|
174
|
+
executeId,
|
|
175
|
+
path,
|
|
176
|
+
headers,
|
|
177
|
+
logger: options.logger,
|
|
178
|
+
detail: options?.detail ?? {}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
let result: { value: Result };
|
|
182
|
+
try {
|
|
183
|
+
// before execute middleware
|
|
184
|
+
await MiddlewareEvent.handle("beforeExecute", [context]);
|
|
185
|
+
|
|
186
|
+
// check type
|
|
187
|
+
// @ts-ignore
|
|
188
|
+
_validate(await (await schema.apiValidator.validate[path]()).params(params));
|
|
189
|
+
|
|
190
|
+
// execute api
|
|
191
|
+
let api: any;
|
|
192
|
+
if (apis.has(path)) api = apis.get(path);
|
|
193
|
+
else {
|
|
194
|
+
// @ts-ignore
|
|
195
|
+
api = schema.apiMethodsSchema[path]();
|
|
196
|
+
apis.set(path, api);
|
|
197
|
+
}
|
|
198
|
+
const apiModuleAwaited = await api.module;
|
|
199
|
+
|
|
200
|
+
const apiMethod = apiModuleAwaited.api.action;
|
|
201
|
+
|
|
202
|
+
// @ts-ignore
|
|
203
|
+
result = { value: await apiMethod(params, context) };
|
|
204
|
+
|
|
205
|
+
// after execute middleware
|
|
206
|
+
await MiddlewareEvent.handle("afterExecute", [context, result]);
|
|
207
|
+
} catch (error: any) {
|
|
208
|
+
const errorResult = hanldeCatchError(error, executeId);
|
|
209
|
+
|
|
210
|
+
return errorResult;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
executeId,
|
|
215
|
+
success: true,
|
|
216
|
+
data: result.value
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function _executeToJson<Path extends keyof (typeof schema)["apiMethodsTypeSchema"]>(path: Path, params: Parameters<(typeof schema)["apiMethodsTypeSchema"][Path]["api"]["action"]>[0] | string, headersInit: Record<string, string> | Headers = {}, options?: ExecuteOptions): Promise<string> {
|
|
221
|
+
const resultsRaw = await _execute(path, params, headersInit, options);
|
|
222
|
+
const results = await (await schema.apiValidator.validate[path]()).results(TSON.encode(resultsRaw));
|
|
223
|
+
return results;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function _executeCoreToJson<Path extends keyof (typeof schema)["apiMethodsTypeSchema"]>(path: Path, params: Parameters<(typeof schema)["apiMethodsTypeSchema"][Path]["api"]["action"]>[0] | string, headersInit: Record<string, string> | Headers = {}, options: ExecuteCoreOptions): Promise<string> {
|
|
227
|
+
const resultsRaw = await _executeCore(path, params, headersInit, options);
|
|
228
|
+
const results = await (await schema.apiValidator.validate[path]()).results(TSON.encode(resultsRaw));
|
|
229
|
+
return results;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const apis = new Map<string, any>();
|
|
233
|
+
|
|
234
|
+
export type ExecuteResult<Result> = ExecuteResultSuccess<Result> | ExecuteResultFail;
|
|
235
|
+
|
|
236
|
+
export type ExecuteResultSuccess<Result> = {
|
|
237
|
+
executeId: ExecuteId;
|
|
238
|
+
success: true;
|
|
239
|
+
data: Result;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export type ExecuteResultFail<FailT extends Fail<keyof FailEnumerates> = Fail<keyof FailEnumerates>> = {
|
|
243
|
+
executeId: ExecuteId;
|
|
244
|
+
success: false;
|
|
245
|
+
fail: FailT;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export type ExecuteOptions = {
|
|
249
|
+
/**
|
|
250
|
+
* The executeId of the request
|
|
251
|
+
* executeId may be generated by the serverless provider, if not, a random string will be generated instead
|
|
252
|
+
*/
|
|
253
|
+
executeId?: string;
|
|
254
|
+
/**
|
|
255
|
+
* Additional information about the request
|
|
256
|
+
* These are usually only fully implemented when called by an HTTP server
|
|
257
|
+
* During testing or when calling between microservices, some or all of the values may be undefined
|
|
258
|
+
*/
|
|
259
|
+
detail?: FrameworkContext["detail"];
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export type ExecuteCoreOptions = Mixin<
|
|
263
|
+
ExecuteOptions,
|
|
264
|
+
{
|
|
265
|
+
executeId: string;
|
|
266
|
+
logger: Logger;
|
|
267
|
+
onAfterHeaders?: (headers: Headers) => void | Promise<void>;
|
|
268
|
+
}
|
|
269
|
+
>;
|
package/kernel/meta.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Meta } from "../../../src/meta";
|
|
2
|
+
import schema from "../../../generate/api-schema";
|
|
3
|
+
|
|
4
|
+
export async function useMeta(path: string): Promise<Meta> {
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
const api = schema.apiMethodsSchema[path as keyof (typeof schema)["apiMethodsTypeSchema"]]();
|
|
7
|
+
const module = await api.module;
|
|
8
|
+
return module.api.meta;
|
|
9
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-type-constraint, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */
|
|
2
|
+
|
|
3
|
+
import type { Context } from "../../../src/context";
|
|
4
|
+
import type { FrameworkHTTPDetail } from "./context";
|
|
5
|
+
import { type LoongbaoApp } from "loongbao";
|
|
6
|
+
|
|
7
|
+
export type BootstrapMiddleware = (loongbao: LoongbaoApp) => Promise<void> | void;
|
|
8
|
+
export type BeforeExecuteMiddleware = (context: Context) => Promise<void> | void;
|
|
9
|
+
export type AfterExecuteMiddleware = (context: Context, response: { value: unknown }) => Promise<void> | void;
|
|
10
|
+
export type AfterHTTPRequestMiddleware = (headers: Headers, detail: FrameworkHTTPDetail) => Promise<void> | void;
|
|
11
|
+
export type BeforeHTTPResponseMiddleware = (response: { value: string }, detail: FrameworkHTTPDetail) => Promise<void> | void;
|
|
12
|
+
export type MiddlewareOptions = {
|
|
13
|
+
bootstrap?: BootstrapMiddleware;
|
|
14
|
+
beforeExecute?: BeforeExecuteMiddleware;
|
|
15
|
+
afterExecute?: AfterExecuteMiddleware;
|
|
16
|
+
afterHTTPRequest?: AfterHTTPRequestMiddleware;
|
|
17
|
+
beforeHTTPResponse?: BeforeHTTPResponseMiddleware;
|
|
18
|
+
} & Record<string, (...args: Array<any>) => Promise<void> | void>;
|
|
19
|
+
export type MiddlewareFn = (...args: Array<any>) => Promise<void> | void;
|
|
20
|
+
export type MiddlewareT<T extends MiddlewareFn = MiddlewareFn> = { id: string; index: number; middleware: T };
|
|
21
|
+
|
|
22
|
+
export const _middlewareEvents = new Map<string, (a: MiddlewareT, b: MiddlewareT) => number>();
|
|
23
|
+
export const _middlewares = new Map<string, Array<MiddlewareT>>();
|
|
24
|
+
|
|
25
|
+
export const MiddlewareEvent = (() => {
|
|
26
|
+
const defineMiddlewareEvent = (name: string, sortFn: (a: MiddlewareT, b: MiddlewareT) => number) => {
|
|
27
|
+
_middlewareEvents.set(name, sortFn);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const sortMiddlewareEvent = () => {
|
|
31
|
+
for (const [key, middleware] of _middlewares) {
|
|
32
|
+
const sort = _middlewareEvents.get(key);
|
|
33
|
+
if (sort) middleware.sort(sort);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleMiddleware = async (name: string, args: Array<any> /* Parameters<MiddlewareOptions[Name]> */) => {
|
|
38
|
+
const mds = _middlewares.get(name);
|
|
39
|
+
if (!mds) return;
|
|
40
|
+
for (const md of mds) {
|
|
41
|
+
await md.middleware(...args);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
define: defineMiddlewareEvent,
|
|
47
|
+
handle: handleMiddleware,
|
|
48
|
+
_sort: sortMiddlewareEvent
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ExecuteId } from "..";
|
|
2
|
+
|
|
3
|
+
export const runtime = {
|
|
4
|
+
execute: {
|
|
5
|
+
executeIds: new Set<ExecuteId>()
|
|
6
|
+
},
|
|
7
|
+
maxRequest: {
|
|
8
|
+
enable: false,
|
|
9
|
+
counter: 0,
|
|
10
|
+
expected: 0
|
|
11
|
+
},
|
|
12
|
+
maxRunningTimeout: {
|
|
13
|
+
enable: false,
|
|
14
|
+
expectedEndedAt: 0
|
|
15
|
+
}
|
|
16
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IValidation } from "typia";
|
|
2
|
+
import { reject } from "../kernel/fail";
|
|
3
|
+
|
|
4
|
+
export function _validate(validator: IValidation.IFailure | IValidation.ISuccess): void {
|
|
5
|
+
if (validator.success) return;
|
|
6
|
+
const error = validator.errors[0];
|
|
7
|
+
|
|
8
|
+
throw reject("general-type-safe-error", {
|
|
9
|
+
path: error.path,
|
|
10
|
+
expected: error.expected,
|
|
11
|
+
value: error.value
|
|
12
|
+
});
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "milkio",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"typescript": "^5.4.2"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@poech/camel-hump-under": "^1.1.0",
|
|
11
|
+
"@southern-aurora/tson": "2.0.2",
|
|
12
|
+
"ulidx": "^2.3.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "latest",
|
|
16
|
+
"walk-sync": "^3.0.0",
|
|
17
|
+
"typia": "5.5.5",
|
|
18
|
+
"ejs": "^3.1.9",
|
|
19
|
+
"@types/ejs": "^3.1.5",
|
|
20
|
+
"@types/js-beautify": "^1.14.3",
|
|
21
|
+
"ts-patch": "^3.1.2",
|
|
22
|
+
"typescript": "^5.3.3"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/* eslint-disable no-console, @typescript-eslint/no-dynamic-delete */
|
|
2
|
+
|
|
3
|
+
import { TSON, type Cookbook } from "..";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { cwd } from "node:process";
|
|
6
|
+
import { writeFile, readFile } from "node:fs/promises";
|
|
7
|
+
|
|
8
|
+
export async function buildCookbook() {
|
|
9
|
+
const schema = await import("../../../generate/api-schema");
|
|
10
|
+
const paths = Object.keys(schema.default.apiMethodsSchema);
|
|
11
|
+
|
|
12
|
+
const cookbook: Cookbook = {};
|
|
13
|
+
for (const path of paths) {
|
|
14
|
+
// const module = await import(/* @vite-ignore */ join(`../../../src/apps/${path}`));
|
|
15
|
+
const code = (await readFile(join(cwd(), `./src/apps/${path}.ts`))).toString();
|
|
16
|
+
const codeLines = code.split("\n");
|
|
17
|
+
let title;
|
|
18
|
+
let desc;
|
|
19
|
+
const descRaw = /\n\/\*\*\n[\s\S]+?\*\//.exec(code)?.[0] ?? "";
|
|
20
|
+
|
|
21
|
+
if (descRaw) {
|
|
22
|
+
const descRawLines = descRaw.split("\n");
|
|
23
|
+
if (descRawLines.at(0)?.trim() === "") descRawLines.shift();
|
|
24
|
+
if (descRawLines.at(-1)?.trim() === "") descRawLines.pop();
|
|
25
|
+
let first = true;
|
|
26
|
+
for (let index = 0; index < descRawLines.length; index++) {
|
|
27
|
+
const descRawLine = descRawLines[index].replace(/^[/ ]+?[*]*/, "").replace(/[*]*\/$/, "");
|
|
28
|
+
|
|
29
|
+
if (!descRawLine) continue;
|
|
30
|
+
if (first) {
|
|
31
|
+
title = descRawLine.replace(/#/g, "").trim();
|
|
32
|
+
// Originally the title was in the first line, desc is the rest of it, now desc contains complete markdown content.
|
|
33
|
+
// continue;
|
|
34
|
+
}
|
|
35
|
+
first = false;
|
|
36
|
+
desc = (desc ?? "") + "\n" + descRawLine.trim();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let apiParams = /action\([\s\S]+?\)/.exec(code)?.[0] ?? ""; // The intention of the following code is to extract the parameter part of the action.
|
|
41
|
+
apiParams = /\([\s\S]*,/.exec(apiParams)?.[0] ?? "";
|
|
42
|
+
apiParams = apiParams.slice(0, -1);
|
|
43
|
+
apiParams = apiParams.slice(/[\s\S]+?:/.exec(apiParams)?.[0].length);
|
|
44
|
+
const apiParamsLines = apiParams.split("\n"); // The intention of the following code is to remove extra spaces, which will make the code look more beautiful.
|
|
45
|
+
if (apiParamsLines.at(-1)?.trim() === "") apiParamsLines.pop();
|
|
46
|
+
if (apiParamsLines.at(-1)?.trim() === "") apiParamsLines.pop();
|
|
47
|
+
let spaceNumber = 0;
|
|
48
|
+
for (const char of apiParamsLines.at(-1) ?? "") {
|
|
49
|
+
if (char === " ") spaceNumber++;
|
|
50
|
+
else break;
|
|
51
|
+
}
|
|
52
|
+
for (let index = 0; index < apiParamsLines.length; index++) {
|
|
53
|
+
const line = apiParamsLines[index];
|
|
54
|
+
let spaceNumberForThisLine = 0;
|
|
55
|
+
for (const char of line) {
|
|
56
|
+
if (char === " ") spaceNumberForThisLine++;
|
|
57
|
+
else break;
|
|
58
|
+
}
|
|
59
|
+
if (spaceNumberForThisLine >= spaceNumber) {
|
|
60
|
+
apiParamsLines[index] = line.slice(spaceNumber);
|
|
61
|
+
} else {
|
|
62
|
+
apiParamsLines[index] = line.slice(spaceNumberForThisLine);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
apiParams = apiParamsLines.join("\n");
|
|
66
|
+
|
|
67
|
+
// Find the code for the API testing section.
|
|
68
|
+
const apiTestsCodeChars = [];
|
|
69
|
+
let apiTestsStartIndex = undefined as undefined | number;
|
|
70
|
+
let semicolonMatch = 0;
|
|
71
|
+
let semicolonMax = 0;
|
|
72
|
+
for (let index = 0; index < codeLines.length; index++) {
|
|
73
|
+
const codeLine = codeLines[index];
|
|
74
|
+
if (apiTestsStartIndex === undefined && !codeLine.includes("defineApiTest(")) continue;
|
|
75
|
+
if (apiTestsStartIndex === undefined) apiTestsStartIndex = index;
|
|
76
|
+
const codeChars = codeLine.split("");
|
|
77
|
+
for (const codeChar of codeChars) {
|
|
78
|
+
if (codeChar === "[") {
|
|
79
|
+
semicolonMatch++;
|
|
80
|
+
semicolonMax++;
|
|
81
|
+
}
|
|
82
|
+
if (semicolonMatch !== 0) apiTestsCodeChars.push(codeChar);
|
|
83
|
+
if (codeChar === "]") semicolonMatch--;
|
|
84
|
+
}
|
|
85
|
+
if (semicolonMatch === 0 && semicolonMax >= 1) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
apiTestsCodeChars.push("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Find the code for each API test case.
|
|
92
|
+
const apiCaseCodes: Array<string> = [];
|
|
93
|
+
let currentApiCaseCode = undefined as undefined | Array<string>;
|
|
94
|
+
let apiTestCaseStartIndex = undefined as undefined | number;
|
|
95
|
+
let apiTestCaseMatch = 0;
|
|
96
|
+
for (let index = 0; index < apiTestsCodeChars.length; index++) {
|
|
97
|
+
const apiTestsCodeChar = apiTestsCodeChars[index];
|
|
98
|
+
if (apiTestCaseStartIndex === undefined && apiTestsCodeChar === "{") {
|
|
99
|
+
currentApiCaseCode = [];
|
|
100
|
+
apiTestCaseStartIndex = index;
|
|
101
|
+
}
|
|
102
|
+
if (apiTestsCodeChar === "{") {
|
|
103
|
+
apiTestCaseMatch++;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (apiTestCaseMatch !== 0) currentApiCaseCode!.push(apiTestsCodeChar);
|
|
107
|
+
|
|
108
|
+
if (apiTestsCodeChar === "}") {
|
|
109
|
+
apiTestCaseMatch--;
|
|
110
|
+
if (apiTestCaseMatch === 0) {
|
|
111
|
+
apiCaseCodes.push(currentApiCaseCode!.join(""));
|
|
112
|
+
currentApiCaseCode = undefined;
|
|
113
|
+
apiTestCaseStartIndex = undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const apiCases: Array<{
|
|
119
|
+
name: string;
|
|
120
|
+
handler: string;
|
|
121
|
+
}> = [];
|
|
122
|
+
|
|
123
|
+
for (let index = 0; index < apiCaseCodes.length; index++) {
|
|
124
|
+
const code = apiCaseCodes[index];
|
|
125
|
+
const name = /name:[\s\S]+?,/.exec(code)?.[0]?.slice(5, -1)?.trim().slice(1, -1) ?? "";
|
|
126
|
+
const handlerChars = /handler:[\s\S]*/.exec(code)?.[0]?.split("") ?? [];
|
|
127
|
+
let handler = ""; // Find the main code of the handler.
|
|
128
|
+
let handlerStartIndex = undefined as undefined | number;
|
|
129
|
+
let handlerMatch = 0;
|
|
130
|
+
for (let index = 0; index < handlerChars.length; index++) {
|
|
131
|
+
const handlerChar = handlerChars[index];
|
|
132
|
+
if (handlerStartIndex !== undefined && handlerChar === "{") handlerStartIndex = index;
|
|
133
|
+
if (handlerChar === "{") handlerMatch++;
|
|
134
|
+
if (handlerMatch !== 0) handler = handler + handlerChar;
|
|
135
|
+
if (handlerChar === "}") handlerMatch--;
|
|
136
|
+
if (handlerStartIndex !== undefined && handlerMatch === 0) break;
|
|
137
|
+
}
|
|
138
|
+
handler = handler.slice(1, -1);
|
|
139
|
+
|
|
140
|
+
const handlerLines = handler.split("\n"); // The intention of the following code is to remove extra spaces, which will make the code look more beautiful.
|
|
141
|
+
if (handlerLines.at(-1)?.trim() === "") handlerLines.pop();
|
|
142
|
+
if (handlerLines.at(-1)?.trim() === "") handlerLines.pop();
|
|
143
|
+
if (handlerLines.at(0)?.trim() === "") handlerLines.shift();
|
|
144
|
+
if (handlerLines.at(0)?.trim() === "") handlerLines.shift();
|
|
145
|
+
let spaceNumber = 0;
|
|
146
|
+
for (const char of handlerLines.at(-1) ?? "") {
|
|
147
|
+
if (char === " ") spaceNumber++;
|
|
148
|
+
else break;
|
|
149
|
+
}
|
|
150
|
+
for (let index = 0; index < handlerLines.length; index++) {
|
|
151
|
+
const line = handlerLines[index];
|
|
152
|
+
let spaceNumberForThisLine = 0;
|
|
153
|
+
for (const char of line) {
|
|
154
|
+
if (char === " ") spaceNumberForThisLine++;
|
|
155
|
+
else break;
|
|
156
|
+
}
|
|
157
|
+
if (spaceNumberForThisLine >= spaceNumber) {
|
|
158
|
+
handlerLines[index] = line.slice(spaceNumber);
|
|
159
|
+
} else {
|
|
160
|
+
handlerLines[index] = line.slice(spaceNumberForThisLine);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
handler = handlerLines.join("\n");
|
|
164
|
+
|
|
165
|
+
apiCases.push({
|
|
166
|
+
name,
|
|
167
|
+
handler
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// This value has been deprecated because TypeScript types can already replace it well
|
|
172
|
+
// let paramsSchema;
|
|
173
|
+
// try {
|
|
174
|
+
// const moduleGenerated = await import(/* @vite-ignore */ `../../../generate/products/apps/${path}`);
|
|
175
|
+
// paramsSchema = moduleGenerated.paramsSchema.schemas[0]?.properties?.data;
|
|
176
|
+
// } catch (error) {}
|
|
177
|
+
|
|
178
|
+
cookbook[path] = {
|
|
179
|
+
title,
|
|
180
|
+
desc,
|
|
181
|
+
params: apiParams,
|
|
182
|
+
cases: apiCases
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* -- indexes
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
const indexes: Record<string, Array<string>> = {};
|
|
191
|
+
const folderIndexes: Record<string, Array<string>> = {};
|
|
192
|
+
indexes["(root)"] = [];
|
|
193
|
+
folderIndexes["(root)"] = [];
|
|
194
|
+
for (const path in cookbook) {
|
|
195
|
+
if (!path.includes("/")) indexes["(root)"].push(path);
|
|
196
|
+
}
|
|
197
|
+
for (const path in cookbook) {
|
|
198
|
+
const dirnames = path.split("/");
|
|
199
|
+
for (let index = 0; index < dirnames.length - 1; index++) {
|
|
200
|
+
const dirpath = dirnames.slice(0, index + 1).join("/");
|
|
201
|
+
if (!indexes[dirpath]) indexes[dirpath] = [];
|
|
202
|
+
if (!folderIndexes[dirpath]) folderIndexes[dirpath] = [];
|
|
203
|
+
if (index + 1 === dirnames.length - 1) {
|
|
204
|
+
indexes[dirpath].push(path);
|
|
205
|
+
} else {
|
|
206
|
+
const childDirpath = dirnames.slice(0, index + 2).join("/");
|
|
207
|
+
if (folderIndexes[dirpath].includes(childDirpath)) continue;
|
|
208
|
+
folderIndexes[dirpath].push(childDirpath);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const path in folderIndexes) {
|
|
213
|
+
if (path.includes("/") || path === "(root)") continue;
|
|
214
|
+
folderIndexes["(root)"].push(path);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const readme = (await readFile(join(cwd(), "src", "apps", "README.md"))).toString();
|
|
218
|
+
Object.keys(indexes).forEach((key) => indexes[key].length === 0 && delete indexes[key]);
|
|
219
|
+
const generatedAt = new Date();
|
|
220
|
+
|
|
221
|
+
await writeFile(
|
|
222
|
+
join(cwd(), `./generate/cookbook.json`),
|
|
223
|
+
TSON.stringify({
|
|
224
|
+
cookbook,
|
|
225
|
+
readme,
|
|
226
|
+
indexes,
|
|
227
|
+
folderIndexes,
|
|
228
|
+
generatedAt
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
void buildCookbook();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { cwd, platform } from "node:process";
|
|
3
|
+
import { exec as nodeExec } from "node:child_process";
|
|
4
|
+
import { removeDir } from "../utils/remove-dir";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { copyFile, mkdir } from "node:fs/promises";
|
|
7
|
+
|
|
8
|
+
export async function buildDTO() {
|
|
9
|
+
console.log("🧊 Loongbao DTO Building..");
|
|
10
|
+
|
|
11
|
+
removeDir(join(cwd(), "packages", "dto", "dist"));
|
|
12
|
+
removeDir(join(cwd(), "packages", "dto", "generate"));
|
|
13
|
+
await mkdir(join(cwd(), "packages", "dto", "dist"));
|
|
14
|
+
await mkdir(join(cwd(), "packages", "dto", "generate"));
|
|
15
|
+
|
|
16
|
+
// Generate the corresponding types for the files in the project and output them to the /packages/dto/generate directory.
|
|
17
|
+
await new Promise((resolve) =>
|
|
18
|
+
nodeExec("bun ./node_modules/typescript/bin/tsc --project tsconfig.build-dto.json", (e, stdout) => {
|
|
19
|
+
resolve(e);
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
await copyFile(join(cwd(), "src", "fail-code.ts"), join(cwd(), "packages", "dto", "generate", "src", "fail-code.ts"));
|
|
23
|
+
|
|
24
|
+
// Packaging type for the dto
|
|
25
|
+
await new Promise((resolve) =>
|
|
26
|
+
nodeExec("cd ./packages/dto && bunx tsc", (e) => {
|
|
27
|
+
resolve(e);
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// build /src/dto/index.ts to js
|
|
32
|
+
await Bun.build({
|
|
33
|
+
entrypoints: ["./packages/dto/index.ts"],
|
|
34
|
+
outdir: "./packages/dto"
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const root = join(cwd(), "packages", "dto");
|
|
38
|
+
|
|
39
|
+
console.log("🧊 Loongbao DTO Build Finished");
|
|
40
|
+
console.log("\x1B[2m");
|
|
41
|
+
console.log("Now, your latest code (including changes to your interface) is built to the latest version and waiting for your release!");
|
|
42
|
+
console.log("");
|
|
43
|
+
console.log("If you want to publish it to NPM, you can use a command similar to the following.");
|
|
44
|
+
console.log(`(But before that, you may need to modify the package name (${join(cwd(), "packages", "dto", "package.json")}) and login to your NPM account or private NPM repository)`);
|
|
45
|
+
|
|
46
|
+
if (platform !== "win32") {
|
|
47
|
+
console.log("You can publish it to npm by running this commands:\n");
|
|
48
|
+
console.log("\u001B[0m---");
|
|
49
|
+
console.log(`cd ${join(root)} \\`);
|
|
50
|
+
console.log(" && npm version major \\");
|
|
51
|
+
console.log(" && npm publish --access public \\");
|
|
52
|
+
console.log(` && cd ${join(cwd())}`);
|
|
53
|
+
} else {
|
|
54
|
+
console.log("You can publish it to npm by running this commands (use \x1B[42mPowerShell\x1B[0m):");
|
|
55
|
+
console.log("\u001B[0m---");
|
|
56
|
+
console.log('$ErrorActionPreference = "Stop";');
|
|
57
|
+
console.log(`Set-Location ${join(root)};`);
|
|
58
|
+
console.log("npm version major;");
|
|
59
|
+
console.log("npm publish --access public;");
|
|
60
|
+
console.log(`Set-Location ${join(cwd())};`);
|
|
61
|
+
}
|
|
62
|
+
console.log("---");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await buildDTO();
|