vitek-plugin 0.1.2-beta.6 → 0.2.1-beta
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/README.md +35 -4
- package/dist/adapters/vite/dev-server-middleware.d.ts +8 -0
- package/dist/adapters/vite/dev-server-middleware.d.ts.map +1 -0
- package/dist/adapters/vite/dev-server-middleware.js +30 -0
- package/dist/adapters/vite/dev-server-state.d.ts +41 -0
- package/dist/adapters/vite/dev-server-state.d.ts.map +1 -0
- package/dist/adapters/vite/dev-server-state.js +191 -0
- package/dist/adapters/vite/dev-server.d.ts +2 -21
- package/dist/adapters/vite/dev-server.d.ts.map +1 -1
- package/dist/adapters/vite/dev-server.js +7 -216
- package/dist/adapters/vite/path-utils.d.ts +20 -0
- package/dist/adapters/vite/path-utils.d.ts.map +1 -0
- package/dist/adapters/vite/path-utils.js +46 -0
- package/dist/adapters/vite/path-utils.test.d.ts +2 -0
- package/dist/adapters/vite/path-utils.test.d.ts.map +1 -0
- package/dist/adapters/vite/path-utils.test.js +79 -0
- package/dist/build/build-api-bundle.d.ts +1 -0
- package/dist/build/build-api-bundle.d.ts.map +1 -1
- package/dist/build/build-api-bundle.js +38 -3
- package/dist/build/build-api-bundle.test.d.ts +2 -0
- package/dist/build/build-api-bundle.test.d.ts.map +1 -0
- package/dist/build/build-api-bundle.test.js +50 -0
- package/dist/build/build-sockets-bundle.test.d.ts +2 -0
- package/dist/build/build-sockets-bundle.test.d.ts.map +1 -0
- package/dist/build/build-sockets-bundle.test.js +49 -0
- package/dist/cli/cli.d.ts +8 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +25 -0
- package/dist/cli/fixtures/serve-config/vitek.config.d.mts +6 -0
- package/dist/cli/fixtures/serve-config/vitek.config.d.mts.map +1 -0
- package/dist/cli/fixtures/serve-config/vitek.config.mjs +19 -0
- package/dist/cli/init.d.ts +15 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +99 -0
- package/dist/cli/init.test.d.ts +2 -0
- package/dist/cli/init.test.d.ts.map +1 -0
- package/dist/cli/init.test.js +117 -0
- package/dist/cli/mcp-project-config.d.ts +8 -0
- package/dist/cli/mcp-project-config.d.ts.map +1 -0
- package/dist/cli/mcp-project-config.js +26 -0
- package/dist/cli/mcp-project.d.ts +2 -0
- package/dist/cli/mcp-project.d.ts.map +1 -0
- package/dist/cli/mcp-project.js +101 -0
- package/dist/cli/serve.d.ts +27 -1
- package/dist/cli/serve.d.ts.map +1 -1
- package/dist/cli/serve.js +85 -10
- package/dist/cli/serve.test.d.ts +2 -0
- package/dist/cli/serve.test.d.ts.map +1 -0
- package/dist/cli/serve.test.js +108 -0
- package/dist/core/asyncapi/generate.test.d.ts +2 -0
- package/dist/core/asyncapi/generate.test.d.ts.map +1 -0
- package/dist/core/asyncapi/generate.test.js +120 -0
- package/dist/core/context/create-context.d.ts +2 -0
- package/dist/core/context/create-context.d.ts.map +1 -1
- package/dist/core/file-system/watch-api-dir.d.ts +4 -1
- package/dist/core/file-system/watch-api-dir.d.ts.map +1 -1
- package/dist/core/file-system/watch-api-dir.js +31 -6
- package/dist/core/file-system/watch-api-dir.test.d.ts +2 -0
- package/dist/core/file-system/watch-api-dir.test.d.ts.map +1 -0
- package/dist/core/file-system/watch-api-dir.test.js +38 -0
- package/dist/core/generation/run-file-generation.d.ts +2 -0
- package/dist/core/generation/run-file-generation.d.ts.map +1 -1
- package/dist/core/generation/run-file-generation.js +4 -1
- package/dist/core/introspection/manifest.d.ts +24 -0
- package/dist/core/introspection/manifest.d.ts.map +1 -0
- package/dist/core/introspection/manifest.js +41 -0
- package/dist/core/introspection/manifest.test.d.ts +2 -0
- package/dist/core/introspection/manifest.test.d.ts.map +1 -0
- package/dist/core/introspection/manifest.test.js +62 -0
- package/dist/core/middleware/get-applicable-middlewares.d.ts +7 -0
- package/dist/core/middleware/get-applicable-middlewares.d.ts.map +1 -1
- package/dist/core/middleware/get-applicable-middlewares.js +23 -15
- package/dist/core/middleware/get-applicable-middlewares.test.js +36 -1
- package/dist/core/openapi/generate.d.ts +3 -79
- package/dist/core/openapi/generate.d.ts.map +1 -1
- package/dist/core/openapi/generate.js +4 -419
- package/dist/core/openapi/generate.test.d.ts +2 -0
- package/dist/core/openapi/generate.test.d.ts.map +1 -0
- package/dist/core/openapi/generate.test.js +184 -0
- package/dist/core/openapi/jsdoc.d.ts +3 -0
- package/dist/core/openapi/jsdoc.d.ts.map +1 -0
- package/dist/core/openapi/jsdoc.js +68 -0
- package/dist/core/openapi/jsdoc.test.d.ts +2 -0
- package/dist/core/openapi/jsdoc.test.d.ts.map +1 -0
- package/dist/core/openapi/jsdoc.test.js +111 -0
- package/dist/core/openapi/spec-builder.d.ts +4 -0
- package/dist/core/openapi/spec-builder.d.ts.map +1 -0
- package/dist/core/openapi/spec-builder.js +257 -0
- package/dist/core/openapi/spec-builder.test.d.ts +2 -0
- package/dist/core/openapi/spec-builder.test.d.ts.map +1 -0
- package/dist/core/openapi/spec-builder.test.js +93 -0
- package/dist/core/openapi/types.d.ts +42 -0
- package/dist/core/openapi/types.d.ts.map +1 -0
- package/dist/core/openapi/types.js +5 -0
- package/dist/core/server/cors.d.ts +29 -0
- package/dist/core/server/cors.d.ts.map +1 -0
- package/dist/core/server/cors.js +55 -0
- package/dist/core/server/cors.test.d.ts +2 -0
- package/dist/core/server/cors.test.d.ts.map +1 -0
- package/dist/core/server/cors.test.js +49 -0
- package/dist/core/server/proxy.d.ts +16 -0
- package/dist/core/server/proxy.d.ts.map +1 -0
- package/dist/core/server/proxy.js +20 -0
- package/dist/core/server/proxy.test.d.ts +2 -0
- package/dist/core/server/proxy.test.d.ts.map +1 -0
- package/dist/core/server/proxy.test.js +53 -0
- package/dist/core/server/request-handler.d.ts +17 -3
- package/dist/core/server/request-handler.d.ts.map +1 -1
- package/dist/core/server/request-handler.js +192 -84
- package/dist/core/server/request-handler.test.js +287 -22
- package/dist/core/socket/socket-handler.test.d.ts +2 -0
- package/dist/core/socket/socket-handler.test.d.ts.map +1 -0
- package/dist/core/socket/socket-handler.test.js +107 -0
- package/dist/core/types/schema.test.d.ts +2 -0
- package/dist/core/types/schema.test.d.ts.map +1 -0
- package/dist/core/types/schema.test.js +41 -0
- package/dist/core/validation/types.d.ts +2 -1
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/validator.d.ts +4 -16
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +4 -16
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/plugin/context.d.ts +15 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +12 -0
- package/dist/plugin/options.d.ts +46 -0
- package/dist/plugin/options.d.ts.map +1 -0
- package/dist/plugin/options.js +1 -0
- package/dist/plugin/plugin-api.d.ts +49 -0
- package/dist/plugin/plugin-api.d.ts.map +1 -0
- package/dist/plugin/plugin-api.js +5 -0
- package/dist/plugin/vitek-build.d.ts +7 -0
- package/dist/plugin/vitek-build.d.ts.map +1 -0
- package/dist/plugin/vitek-build.js +104 -0
- package/dist/plugin/vitek-config.d.ts +4 -0
- package/dist/plugin/vitek-config.d.ts.map +1 -0
- package/dist/plugin/vitek-config.js +51 -0
- package/dist/plugin/vitek-config.test.d.ts +2 -0
- package/dist/plugin/vitek-config.test.d.ts.map +1 -0
- package/dist/plugin/vitek-config.test.js +62 -0
- package/dist/plugin/vitek-dev.d.ts +7 -0
- package/dist/plugin/vitek-dev.d.ts.map +1 -0
- package/dist/plugin/vitek-dev.js +71 -0
- package/dist/plugin/vitek-preview.d.ts +7 -0
- package/dist/plugin/vitek-preview.d.ts.map +1 -0
- package/dist/plugin/vitek-preview.js +107 -0
- package/dist/plugin/vitek-resolve.d.ts +7 -0
- package/dist/plugin/vitek-resolve.d.ts.map +1 -0
- package/dist/plugin/vitek-resolve.js +26 -0
- package/dist/plugin/vitek-transform.d.ts +7 -0
- package/dist/plugin/vitek-transform.d.ts.map +1 -0
- package/dist/plugin/vitek-transform.js +80 -0
- package/dist/plugin/vitek.d.ts +10 -0
- package/dist/plugin/vitek.d.ts.map +1 -0
- package/dist/plugin/vitek.js +27 -0
- package/dist/plugin.d.ts +3 -32
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +2 -274
- package/dist/plugin.test.js +99 -29
- package/dist/shared/response-helpers.d.ts +21 -0
- package/dist/shared/response-helpers.d.ts.map +1 -1
- package/dist/shared/response-helpers.js +41 -0
- package/dist/shared/response-helpers.test.js +54 -1
- package/package.json +32 -15
|
@@ -9,32 +9,57 @@ import { ROUTE_FILE_PATTERN, MIDDLEWARE_FILE_PATTERN } from '../../shared/consta
|
|
|
9
9
|
* Creates a watcher for the API directory using Node.js fs.watch
|
|
10
10
|
* Returns a function to stop watching
|
|
11
11
|
*/
|
|
12
|
-
export function watchApiDirectory(apiDir, callback) {
|
|
12
|
+
export function watchApiDirectory(apiDir, callback, options = {}) {
|
|
13
13
|
if (!fs.existsSync(apiDir)) {
|
|
14
14
|
return { close: () => { } };
|
|
15
15
|
}
|
|
16
|
+
const debounceMs = options.debounceMs ?? 0;
|
|
17
|
+
let pending = [];
|
|
18
|
+
let timer = null;
|
|
19
|
+
const flush = () => {
|
|
20
|
+
if (timer != null) {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
timer = null;
|
|
23
|
+
}
|
|
24
|
+
const toEmit = pending;
|
|
25
|
+
pending = [];
|
|
26
|
+
for (const { event, filePath } of toEmit) {
|
|
27
|
+
callback(event, filePath);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const schedule = (event, filePath) => {
|
|
31
|
+
pending.push({ event, filePath });
|
|
32
|
+
if (debounceMs <= 0) {
|
|
33
|
+
flush();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (timer != null)
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
timer = setTimeout(flush, debounceMs);
|
|
39
|
+
};
|
|
16
40
|
const watcher = fs.watch(apiDir, { recursive: true }, (eventType, filename) => {
|
|
17
41
|
if (!filename)
|
|
18
42
|
return;
|
|
19
43
|
const filePath = path.join(apiDir, filename);
|
|
20
|
-
// Ignore if it's not a route file or relevant middleware
|
|
21
44
|
const isRouteFile = ROUTE_FILE_PATTERN.test(filename);
|
|
22
45
|
const isMiddlewareFile = MIDDLEWARE_FILE_PATTERN.test(filename);
|
|
23
46
|
if (!isRouteFile && !isMiddlewareFile) {
|
|
24
47
|
return;
|
|
25
48
|
}
|
|
26
|
-
// Normalize event type
|
|
27
49
|
let normalizedEvent;
|
|
28
50
|
if (eventType === 'rename') {
|
|
29
|
-
// rename can be add or unlink, check if it exists
|
|
30
51
|
normalizedEvent = fs.existsSync(filePath) ? 'add' : 'unlink';
|
|
31
52
|
}
|
|
32
53
|
else {
|
|
33
54
|
normalizedEvent = eventType;
|
|
34
55
|
}
|
|
35
|
-
|
|
56
|
+
schedule(normalizedEvent, filePath);
|
|
36
57
|
});
|
|
37
58
|
return {
|
|
38
|
-
close: () =>
|
|
59
|
+
close: () => {
|
|
60
|
+
if (timer != null)
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
watcher.close();
|
|
63
|
+
},
|
|
39
64
|
};
|
|
40
65
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-api-dir.test.d.ts","sourceRoot":"","sources":["../../../src/core/file-system/watch-api-dir.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { watchApiDirectory } from './watch-api-dir.js';
|
|
5
|
+
describe('watch-api-dir', () => {
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let apiDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(process.cwd(), 'watch-api-'));
|
|
10
|
+
apiDir = path.join(tmpDir, 'api');
|
|
11
|
+
fs.mkdirSync(apiDir, { recursive: true });
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
try {
|
|
15
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// ignore
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
it('returns watcher with no-op close when apiDir does not exist', () => {
|
|
22
|
+
const watcher = watchApiDirectory(path.join(tmpDir, 'nonexistent'), () => { });
|
|
23
|
+
expect(watcher).toBeDefined();
|
|
24
|
+
expect(typeof watcher.close).toBe('function');
|
|
25
|
+
watcher.close();
|
|
26
|
+
});
|
|
27
|
+
it('returns watcher with close when apiDir exists', () => {
|
|
28
|
+
const watcher = watchApiDirectory(apiDir, () => { });
|
|
29
|
+
expect(watcher).toBeDefined();
|
|
30
|
+
expect(typeof watcher.close).toBe('function');
|
|
31
|
+
watcher.close();
|
|
32
|
+
});
|
|
33
|
+
it('accepts debounceMs option', () => {
|
|
34
|
+
const watcher = watchApiDirectory(apiDir, () => { }, { debounceMs: 100 });
|
|
35
|
+
expect(watcher).toBeDefined();
|
|
36
|
+
watcher.close();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -17,6 +17,8 @@ export interface RunFileGenerationOptions {
|
|
|
17
17
|
info?: (message: string) => void;
|
|
18
18
|
warn?: (message: string) => void;
|
|
19
19
|
};
|
|
20
|
+
/** Callback when generation fails (e.g. OpenAPI). Does not run for types/services write errors. */
|
|
21
|
+
onGenerationError?: (error: Error) => void;
|
|
20
22
|
}
|
|
21
23
|
export declare function runFileGeneration(options: RunFileGenerationOptions): Promise<void>;
|
|
22
24
|
//# sourceMappingURL=run-file-generation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-file-generation.d.ts","sourceRoot":"","sources":["../../../src/core/generation/run-file-generation.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAKtD,OAAO,EAA4C,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAgBvG,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,CAS/E;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE;QACP,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;QACxC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;QAC3C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;QACjC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;KAClC,CAAC;
|
|
1
|
+
{"version":3,"file":"run-file-generation.d.ts","sourceRoot":"","sources":["../../../src/core/generation/run-file-generation.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAKtD,OAAO,EAA4C,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAgBvG,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,CAS/E;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE;QACP,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;QACxC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;QAC3C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;QACjC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;KAClC,CAAC;IACF,mGAAmG;IACnG,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAC5C;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuFxF"}
|
|
@@ -28,6 +28,7 @@ export async function runFileGeneration(options) {
|
|
|
28
28
|
const logServices = logger?.servicesGenerated ?? (() => { });
|
|
29
29
|
const logInfo = logger?.info ?? (() => { });
|
|
30
30
|
const logWarn = logger?.warn ?? (() => { });
|
|
31
|
+
const onGenerationError = options.onGenerationError;
|
|
31
32
|
const isTypeScript = isTypeScriptProject(root);
|
|
32
33
|
const srcDir = path.join(root, 'src');
|
|
33
34
|
if (schema.length > 0) {
|
|
@@ -81,7 +82,9 @@ export async function runFileGeneration(options) {
|
|
|
81
82
|
logInfo(`API docs at: ./${path.relative(root, apiDocsPath).replace(/\\/g, '/')} → http://localhost:${serverPort}/api-docs.html`);
|
|
82
83
|
}
|
|
83
84
|
catch (error) {
|
|
84
|
-
|
|
85
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
86
|
+
logWarn(`Failed to generate OpenAPI spec: ${err.message}`);
|
|
87
|
+
onGenerationError?.(err);
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ParsedRoute } from '../routing/route-parser.js';
|
|
2
|
+
import type { ParsedSocket } from '../routing/socket-parser.js';
|
|
3
|
+
export interface VitekManifest {
|
|
4
|
+
routes: Array<{
|
|
5
|
+
method: string;
|
|
6
|
+
pattern: string;
|
|
7
|
+
params: string[];
|
|
8
|
+
file: string;
|
|
9
|
+
}>;
|
|
10
|
+
middlewares: Array<{
|
|
11
|
+
basePattern: string;
|
|
12
|
+
path: string;
|
|
13
|
+
}>;
|
|
14
|
+
sockets: Array<{
|
|
15
|
+
pattern: string;
|
|
16
|
+
params: string[];
|
|
17
|
+
file: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export declare function getManifest(root: string, apiDir: string): VitekManifest;
|
|
21
|
+
export declare function getRoutes(root: string, apiDir: string): ParsedRoute[];
|
|
22
|
+
export declare function getSockets(root: string, apiDir: string): ParsedSocket[];
|
|
23
|
+
export declare function writeManifest(root: string, apiDir: string, outDir: string): string;
|
|
24
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/core/introspection/manifest.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEhE,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,KAAK,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC,CAAC;IACH,WAAW,EAAE,KAAK,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC,CAAC;IACH,OAAO,EAAE,KAAK,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC,CAAC;CACJ;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CAoBvE;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,EAAE,CAIrE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CAIvE;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,MAAM,CAMR"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { scanApiDirectory } from '../file-system/scan-api-dir.js';
|
|
4
|
+
export function getManifest(root, apiDir) {
|
|
5
|
+
const fullApiDir = path.isAbsolute(apiDir) ? apiDir : path.resolve(root, apiDir);
|
|
6
|
+
const scanResult = scanApiDirectory(fullApiDir);
|
|
7
|
+
return {
|
|
8
|
+
routes: scanResult.routes.map((r) => ({
|
|
9
|
+
method: r.method,
|
|
10
|
+
pattern: r.pattern,
|
|
11
|
+
params: r.params,
|
|
12
|
+
file: path.relative(root, r.file).replace(/\\/g, '/'),
|
|
13
|
+
})),
|
|
14
|
+
middlewares: scanResult.middlewares.map((m) => ({
|
|
15
|
+
basePattern: m.basePattern,
|
|
16
|
+
path: path.relative(root, m.path).replace(/\\/g, '/'),
|
|
17
|
+
})),
|
|
18
|
+
sockets: scanResult.sockets.map((s) => ({
|
|
19
|
+
pattern: s.pattern,
|
|
20
|
+
params: s.params,
|
|
21
|
+
file: path.relative(root, s.file).replace(/\\/g, '/'),
|
|
22
|
+
})),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function getRoutes(root, apiDir) {
|
|
26
|
+
const fullApiDir = path.isAbsolute(apiDir) ? apiDir : path.resolve(root, apiDir);
|
|
27
|
+
const scanResult = scanApiDirectory(fullApiDir);
|
|
28
|
+
return scanResult.routes;
|
|
29
|
+
}
|
|
30
|
+
export function getSockets(root, apiDir) {
|
|
31
|
+
const fullApiDir = path.isAbsolute(apiDir) ? apiDir : path.resolve(root, apiDir);
|
|
32
|
+
const scanResult = scanApiDirectory(fullApiDir);
|
|
33
|
+
return scanResult.sockets;
|
|
34
|
+
}
|
|
35
|
+
export function writeManifest(root, apiDir, outDir) {
|
|
36
|
+
const manifest = getManifest(root, apiDir);
|
|
37
|
+
const manifestPath = path.join(outDir, 'vitek-manifest.json');
|
|
38
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
39
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
40
|
+
return manifestPath;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.test.d.ts","sourceRoot":"","sources":["../../../src/core/introspection/manifest.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { getManifest, getRoutes, getSockets, writeManifest } from './manifest.js';
|
|
5
|
+
describe('manifest', () => {
|
|
6
|
+
let rootDir;
|
|
7
|
+
let apiDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
rootDir = fs.mkdtempSync(path.join(process.cwd(), 'vitek-manifest-test-'));
|
|
10
|
+
apiDir = path.join(rootDir, 'src', 'api');
|
|
11
|
+
fs.mkdirSync(apiDir, { recursive: true });
|
|
12
|
+
fs.writeFileSync(path.join(apiDir, 'health.get.ts'), 'export default () => ({});');
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
try {
|
|
16
|
+
fs.rmSync(rootDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// ignore cleanup errors
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
it('getManifest returns routes, middlewares, sockets', () => {
|
|
23
|
+
const manifest = getManifest(rootDir, 'src/api');
|
|
24
|
+
expect(manifest).toHaveProperty('routes');
|
|
25
|
+
expect(manifest).toHaveProperty('middlewares');
|
|
26
|
+
expect(manifest).toHaveProperty('sockets');
|
|
27
|
+
expect(Array.isArray(manifest.routes)).toBe(true);
|
|
28
|
+
expect(Array.isArray(manifest.middlewares)).toBe(true);
|
|
29
|
+
expect(Array.isArray(manifest.sockets)).toBe(true);
|
|
30
|
+
expect(manifest.routes.length).toBeGreaterThan(0);
|
|
31
|
+
});
|
|
32
|
+
it('getRoutes returns ParsedRoute[]', () => {
|
|
33
|
+
const routes = getRoutes(rootDir, 'src/api');
|
|
34
|
+
expect(Array.isArray(routes)).toBe(true);
|
|
35
|
+
routes.forEach((r) => {
|
|
36
|
+
expect(r).toHaveProperty('method');
|
|
37
|
+
expect(r).toHaveProperty('pattern');
|
|
38
|
+
expect(r).toHaveProperty('params');
|
|
39
|
+
expect(r).toHaveProperty('file');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
it('getSockets returns ParsedSocket[]', () => {
|
|
43
|
+
const sockets = getSockets(rootDir, 'src/api');
|
|
44
|
+
expect(Array.isArray(sockets)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('getManifest with non-existent apiDir returns empty arrays', () => {
|
|
47
|
+
const manifest = getManifest(rootDir, 'non-existent-dir');
|
|
48
|
+
expect(manifest.routes).toEqual([]);
|
|
49
|
+
expect(manifest.middlewares).toEqual([]);
|
|
50
|
+
expect(manifest.sockets).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
it('writeManifest writes vitek-manifest.json', () => {
|
|
53
|
+
const outDir = path.join(rootDir, 'dist');
|
|
54
|
+
const written = writeManifest(rootDir, 'src/api', outDir);
|
|
55
|
+
expect(written).toBe(path.join(outDir, 'vitek-manifest.json'));
|
|
56
|
+
expect(fs.existsSync(written)).toBe(true);
|
|
57
|
+
const content = JSON.parse(fs.readFileSync(written, 'utf-8'));
|
|
58
|
+
expect(content).toHaveProperty('routes');
|
|
59
|
+
expect(content).toHaveProperty('middlewares');
|
|
60
|
+
expect(content).toHaveProperty('sockets');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -9,7 +9,14 @@ import type { Middleware } from '../routing/route-types.js';
|
|
|
9
9
|
export interface LoadedMiddleware {
|
|
10
10
|
middleware: Middleware[];
|
|
11
11
|
basePattern: string;
|
|
12
|
+
/** Optional path matcher for global middleware (basePattern ''). Only routes matching one of these patterns get this middleware. E.g. ['protected/*', 'admin'] */
|
|
13
|
+
pathPatterns?: string[];
|
|
12
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Returns true if routePattern matches a single path pattern.
|
|
17
|
+
* Pattern may be exact ('admin') or prefix with * ('admin/*'). No leading slash.
|
|
18
|
+
*/
|
|
19
|
+
export declare function matchPathPattern(routePattern: string, configPattern: string): boolean;
|
|
13
20
|
/**
|
|
14
21
|
* Returns the middlewares applicable to a route, ordered from most generic to most specific
|
|
15
22
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-applicable-middlewares.d.ts","sourceRoot":"","sources":["../../../src/core/middleware/get-applicable-middlewares.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,UAAU,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"get-applicable-middlewares.d.ts","sourceRoot":"","sources":["../../../src/core/middleware/get-applicable-middlewares.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,UAAU,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,kKAAkK;IAClK,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAQrF;AA+BD;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,gBAAgB,EAAE,EAC/B,YAAY,EAAE,MAAM,GACnB,UAAU,EAAE,CAUd"}
|
|
@@ -2,35 +2,43 @@
|
|
|
2
2
|
* Determines which middlewares are applicable to a route based on the pattern
|
|
3
3
|
* Core logic - no dependencies
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Returns true if routePattern matches a single path pattern.
|
|
7
|
+
* Pattern may be exact ('admin') or prefix with * ('admin/*'). No leading slash.
|
|
8
|
+
*/
|
|
9
|
+
export function matchPathPattern(routePattern, configPattern) {
|
|
10
|
+
const route = routePattern.replace(/^\/+|\/+$/g, '');
|
|
11
|
+
const pattern = configPattern.replace(/^\/+|\/+$/g, '');
|
|
12
|
+
if (pattern.endsWith('/*')) {
|
|
13
|
+
const prefix = pattern.slice(0, -2);
|
|
14
|
+
return route === prefix || route.startsWith(prefix + '/');
|
|
15
|
+
}
|
|
16
|
+
return route === pattern || route.startsWith(pattern + '/');
|
|
17
|
+
}
|
|
5
18
|
/**
|
|
6
19
|
* Checks if a middleware base pattern applies to a route pattern
|
|
7
20
|
* A middleware applies if its basePattern is a prefix of the routePattern
|
|
8
|
-
*
|
|
9
|
-
* Examples:
|
|
10
|
-
* - basePattern: "" → applies to all routes (global)
|
|
11
|
-
* - basePattern: "posts" → applies to "posts", "posts/:id", "posts/:id/comments", etc
|
|
12
|
-
* - basePattern: "posts/:id" → applies to "posts/:id", "posts/:id/comments", but not to "posts"
|
|
21
|
+
* For global middleware (basePattern '') with pathPatterns, applies only if route matches one of them.
|
|
13
22
|
*/
|
|
14
|
-
function isMiddlewareApplicable(
|
|
15
|
-
|
|
23
|
+
function isMiddlewareApplicable(loaded, routePattern) {
|
|
24
|
+
const { basePattern, pathPatterns } = loaded;
|
|
25
|
+
// Global middleware (empty basePattern)
|
|
16
26
|
if (basePattern === '') {
|
|
27
|
+
if (pathPatterns != null && pathPatterns.length > 0) {
|
|
28
|
+
return pathPatterns.some((p) => matchPathPattern(routePattern, p));
|
|
29
|
+
}
|
|
17
30
|
return true;
|
|
18
31
|
}
|
|
19
32
|
// If route is empty (root), only global middleware applies
|
|
20
33
|
if (routePattern === '') {
|
|
21
34
|
return false;
|
|
22
35
|
}
|
|
23
|
-
// Normalize patterns for comparison (remove leading/trailing slashes)
|
|
24
36
|
const normalizedBase = basePattern.replace(/^\/+|\/+$/g, '');
|
|
25
37
|
const normalizedRoute = routePattern.replace(/^\/+|\/+$/g, '');
|
|
26
|
-
|
|
27
|
-
// Or if route starts with basePattern followed by /
|
|
28
|
-
if (normalizedRoute === normalizedBase) {
|
|
38
|
+
if (normalizedRoute === normalizedBase)
|
|
29
39
|
return true;
|
|
30
|
-
|
|
31
|
-
if (normalizedRoute.startsWith(normalizedBase + '/')) {
|
|
40
|
+
if (normalizedRoute.startsWith(normalizedBase + '/'))
|
|
32
41
|
return true;
|
|
33
|
-
}
|
|
34
42
|
return false;
|
|
35
43
|
}
|
|
36
44
|
/**
|
|
@@ -39,7 +47,7 @@ function isMiddlewareApplicable(basePattern, routePattern) {
|
|
|
39
47
|
export function getApplicableMiddlewares(middlewares, routePattern) {
|
|
40
48
|
const applicable = [];
|
|
41
49
|
for (const loadedMiddleware of middlewares) {
|
|
42
|
-
if (isMiddlewareApplicable(loadedMiddleware
|
|
50
|
+
if (isMiddlewareApplicable(loadedMiddleware, routePattern)) {
|
|
43
51
|
applicable.push(...loadedMiddleware.middleware);
|
|
44
52
|
}
|
|
45
53
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { getApplicableMiddlewares } from './get-applicable-middlewares.js';
|
|
2
|
+
import { getApplicableMiddlewares, matchPathPattern } from './get-applicable-middlewares.js';
|
|
3
3
|
function noopMiddleware() {
|
|
4
4
|
return async (_ctx, next) => next();
|
|
5
5
|
}
|
|
@@ -71,4 +71,39 @@ describe('getApplicableMiddlewares', () => {
|
|
|
71
71
|
];
|
|
72
72
|
expect(getApplicableMiddlewares(middlewares, 'posts/1')).toEqual([mw1, mw2]);
|
|
73
73
|
});
|
|
74
|
+
describe('global middleware with pathPatterns', () => {
|
|
75
|
+
it('applies only to routes matching pathPatterns when set', () => {
|
|
76
|
+
const globalMw = noopMiddleware();
|
|
77
|
+
const middlewares = [
|
|
78
|
+
{ basePattern: '', middleware: [globalMw], pathPatterns: ['protected/*', 'admin'] },
|
|
79
|
+
];
|
|
80
|
+
expect(getApplicableMiddlewares(middlewares, 'protected')).toEqual([globalMw]);
|
|
81
|
+
expect(getApplicableMiddlewares(middlewares, 'protected/1')).toEqual([globalMw]);
|
|
82
|
+
expect(getApplicableMiddlewares(middlewares, 'admin')).toEqual([globalMw]);
|
|
83
|
+
expect(getApplicableMiddlewares(middlewares, 'admin/users')).toEqual([globalMw]);
|
|
84
|
+
expect(getApplicableMiddlewares(middlewares, 'posts')).toEqual([]);
|
|
85
|
+
expect(getApplicableMiddlewares(middlewares, 'health')).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
it('global without pathPatterns still applies to all routes', () => {
|
|
88
|
+
const globalMw = noopMiddleware();
|
|
89
|
+
const middlewares = [
|
|
90
|
+
{ basePattern: '', middleware: [globalMw] },
|
|
91
|
+
];
|
|
92
|
+
expect(getApplicableMiddlewares(middlewares, 'posts')).toEqual([globalMw]);
|
|
93
|
+
expect(getApplicableMiddlewares(middlewares, 'any/route')).toEqual([globalMw]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('matchPathPattern', () => {
|
|
98
|
+
it('matches exact pattern', () => {
|
|
99
|
+
expect(matchPathPattern('admin', 'admin')).toBe(true);
|
|
100
|
+
expect(matchPathPattern('admin/users', 'admin')).toBe(true);
|
|
101
|
+
expect(matchPathPattern('posts', 'admin')).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
it('matches glob pattern with /*', () => {
|
|
104
|
+
expect(matchPathPattern('protected', 'protected/*')).toBe(true);
|
|
105
|
+
expect(matchPathPattern('protected/1', 'protected/*')).toBe(true);
|
|
106
|
+
expect(matchPathPattern('protected/1/2', 'protected/*')).toBe(true);
|
|
107
|
+
expect(matchPathPattern('posts', 'protected/*')).toBe(false);
|
|
108
|
+
});
|
|
74
109
|
});
|
|
@@ -1,83 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Core logic - no Vite dependencies
|
|
4
|
-
*/
|
|
5
|
-
export type RouteForDocs = {
|
|
6
|
-
pattern: string;
|
|
7
|
-
method: string;
|
|
8
|
-
params: string[];
|
|
9
|
-
file: string;
|
|
10
|
-
bodyType?: string;
|
|
11
|
-
queryType?: string;
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
|
-
* OpenAPI Info object
|
|
15
|
-
*/
|
|
16
|
-
export interface OpenApiInfo {
|
|
17
|
-
title: string;
|
|
18
|
-
version: string;
|
|
19
|
-
description?: string;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* OpenAPI Server object
|
|
23
|
-
*/
|
|
24
|
-
export interface OpenApiServer {
|
|
25
|
-
url: string;
|
|
26
|
-
description?: string;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* OpenAPI specification options
|
|
30
|
-
*/
|
|
31
|
-
export interface OpenApiOptions {
|
|
32
|
-
/** API information. Uses defaults if not provided. */
|
|
33
|
-
info?: OpenApiInfo;
|
|
34
|
-
/** Server URLs for the API. Defaults to current URL if not provided. */
|
|
35
|
-
servers?: OpenApiServer[];
|
|
36
|
-
/** Base path for API routes. Defaults to "/api". */
|
|
37
|
-
apiBasePath?: string;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* JSDoc metadata extracted from a route file
|
|
41
|
-
*/
|
|
42
|
-
export interface RouteMetadata {
|
|
43
|
-
summary?: string;
|
|
44
|
-
description?: string;
|
|
45
|
-
tags?: string[];
|
|
46
|
-
deprecated?: boolean;
|
|
47
|
-
responses?: Record<string, ResponseMetadata>;
|
|
48
|
-
bodyDescription?: string;
|
|
49
|
-
queryDescription?: string;
|
|
50
|
-
paramDescriptions?: Record<string, string>;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Response metadata from JSDoc
|
|
54
|
-
*/
|
|
55
|
-
export interface ResponseMetadata {
|
|
56
|
-
description: string;
|
|
57
|
-
type?: string;
|
|
58
|
-
example?: unknown;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Generates a complete OpenAPI 3.0 specification
|
|
62
|
-
*/
|
|
1
|
+
import { type RouteForDocs, type OpenApiOptions } from './types.js';
|
|
2
|
+
export type { RouteForDocs, OpenApiInfo, OpenApiServer, OpenApiOptions, RouteMetadata, ResponseMetadata, ApiDocsHtmlOptions } from './types.js';
|
|
63
3
|
export declare function generateOpenApiSpec(routes: RouteForDocs[], options: OpenApiOptions): object;
|
|
64
|
-
/**
|
|
65
|
-
* Generates the OpenAPI JSON file
|
|
66
|
-
*/
|
|
67
4
|
export declare function generateOpenApiFile(outputPath: string, routes: RouteForDocs[], options: OpenApiOptions): Promise<void>;
|
|
68
|
-
/**
|
|
69
|
-
* Generates Swagger UI HTML
|
|
70
|
-
*/
|
|
71
5
|
export declare function generateSwaggerUiHtml(apiJsonPath: string, title?: string): string;
|
|
72
|
-
|
|
73
|
-
* Options for combined API docs (REST + WebSockets)
|
|
74
|
-
*/
|
|
75
|
-
export interface ApiDocsHtmlOptions {
|
|
76
|
-
/** Path to AsyncAPI JSON (e.g. /asyncapi.json). When set, adds WebSockets tab. */
|
|
77
|
-
asyncApiJsonPath?: string;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Generates combined API documentation HTML with optional REST (Swagger) and WebSockets (AsyncAPI) tabs
|
|
81
|
-
*/
|
|
82
|
-
export declare function generateApiDocsHtml(openApiJsonPath: string, title: string, options?: ApiDocsHtmlOptions): string;
|
|
6
|
+
export declare function generateApiDocsHtml(openApiJsonPath: string, title: string, options?: import('./types.js').ApiDocsHtmlOptions): string;
|
|
83
7
|
//# sourceMappingURL=generate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/core/openapi/generate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/core/openapi/generate.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwB,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAG1F,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhJ,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,CAiB3F;AAED,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,YAAY,EAAE,EACtB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CASf;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,GAAE,MAA4B,GAAG,MAAM,CAwBtG;AAED,wBAAgB,mBAAmB,CACjC,eAAe,EAAE,MAAM,EACvB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,OAAO,YAAY,EAAE,kBAAuB,GACpD,MAAM,CAuER"}
|