vitek-plugin 0.1.0-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/LICENSE +21 -0
- package/README.md +908 -0
- package/dist/adapters/vite/dev-server.d.ts +23 -0
- package/dist/adapters/vite/dev-server.d.ts.map +1 -0
- package/dist/adapters/vite/dev-server.js +428 -0
- package/dist/adapters/vite/logger.d.ts +33 -0
- package/dist/adapters/vite/logger.d.ts.map +1 -0
- package/dist/adapters/vite/logger.js +112 -0
- package/dist/core/context/create-context.d.ts +39 -0
- package/dist/core/context/create-context.d.ts.map +1 -0
- package/dist/core/context/create-context.js +34 -0
- package/dist/core/file-system/scan-api-dir.d.ts +26 -0
- package/dist/core/file-system/scan-api-dir.d.ts.map +1 -0
- package/dist/core/file-system/scan-api-dir.js +83 -0
- package/dist/core/file-system/watch-api-dir.d.ts +18 -0
- package/dist/core/file-system/watch-api-dir.d.ts.map +1 -0
- package/dist/core/file-system/watch-api-dir.js +40 -0
- package/dist/core/middleware/compose.d.ts +12 -0
- package/dist/core/middleware/compose.d.ts.map +1 -0
- package/dist/core/middleware/compose.js +27 -0
- package/dist/core/middleware/get-applicable-middlewares.d.ts +17 -0
- package/dist/core/middleware/get-applicable-middlewares.d.ts.map +1 -0
- package/dist/core/middleware/get-applicable-middlewares.js +47 -0
- package/dist/core/middleware/load-global.d.ts +13 -0
- package/dist/core/middleware/load-global.d.ts.map +1 -0
- package/dist/core/middleware/load-global.js +37 -0
- package/dist/core/normalize/normalize-path.d.ts +25 -0
- package/dist/core/normalize/normalize-path.d.ts.map +1 -0
- package/dist/core/normalize/normalize-path.js +76 -0
- package/dist/core/routing/route-matcher.d.ts +10 -0
- package/dist/core/routing/route-matcher.d.ts.map +1 -0
- package/dist/core/routing/route-matcher.js +32 -0
- package/dist/core/routing/route-parser.d.ts +28 -0
- package/dist/core/routing/route-parser.d.ts.map +1 -0
- package/dist/core/routing/route-parser.js +52 -0
- package/dist/core/routing/route-types.d.ts +43 -0
- package/dist/core/routing/route-types.d.ts.map +1 -0
- package/dist/core/routing/route-types.js +5 -0
- package/dist/core/types/extract-ast.d.ts +18 -0
- package/dist/core/types/extract-ast.d.ts.map +1 -0
- package/dist/core/types/extract-ast.js +26 -0
- package/dist/core/types/generate.d.ts +22 -0
- package/dist/core/types/generate.d.ts.map +1 -0
- package/dist/core/types/generate.js +576 -0
- package/dist/core/types/schema.d.ts +21 -0
- package/dist/core/types/schema.d.ts.map +1 -0
- package/dist/core/types/schema.js +17 -0
- package/dist/core/validation/types.d.ts +27 -0
- package/dist/core/validation/types.d.ts.map +1 -0
- package/dist/core/validation/types.js +5 -0
- package/dist/core/validation/validator.d.ts +22 -0
- package/dist/core/validation/validator.d.ts.map +1 -0
- package/dist/core/validation/validator.js +131 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/plugin.d.ts +27 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +54 -0
- package/dist/shared/constants.d.ts +13 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +12 -0
- package/dist/shared/errors.d.ts +75 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +118 -0
- package/dist/shared/response-helpers.d.ts +61 -0
- package/dist/shared/response-helpers.d.ts.map +1 -0
- package/dist/shared/response-helpers.js +100 -0
- package/dist/shared/utils.d.ts +17 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +27 -0
- package/package.json +48 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context creation for route handlers
|
|
3
|
+
* Core logic - runtime agnostic
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Type guard to check if a value is a VitekResponse
|
|
7
|
+
* A VitekResponse is identified by having 'status' (number) or 'headers' (object) properties
|
|
8
|
+
* Plain objects without these properties are treated as regular JSON responses (backward compatibility)
|
|
9
|
+
*/
|
|
10
|
+
export function isVitekResponse(value) {
|
|
11
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
// It's a VitekResponse if it has 'status' (number) or 'headers' (object) properties
|
|
15
|
+
// These are clear indicators of a response object, not a data object
|
|
16
|
+
const hasStatus = 'status' in value && typeof value.status === 'number';
|
|
17
|
+
const hasHeaders = 'headers' in value && typeof value.headers === 'object' && value.headers !== null && !Array.isArray(value.headers);
|
|
18
|
+
return hasStatus || hasHeaders;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Creates a context from a request and extracted parameters
|
|
22
|
+
*/
|
|
23
|
+
export function createContext(request, params = {}, query = {}) {
|
|
24
|
+
const url = new URL(request.url, 'http://localhost');
|
|
25
|
+
return {
|
|
26
|
+
url: request.url,
|
|
27
|
+
method: request.method.toLowerCase(),
|
|
28
|
+
path: url.pathname,
|
|
29
|
+
query,
|
|
30
|
+
params,
|
|
31
|
+
headers: request.headers,
|
|
32
|
+
body: request.body,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API directory scanner
|
|
3
|
+
* Core logic - uses Node.js fs but abstracted to be testable
|
|
4
|
+
*/
|
|
5
|
+
import type { ParsedRoute } from '../routing/route-parser.js';
|
|
6
|
+
/**
|
|
7
|
+
* Information about a found middleware
|
|
8
|
+
*/
|
|
9
|
+
export interface MiddlewareInfo {
|
|
10
|
+
/** Absolute path of the middleware file */
|
|
11
|
+
path: string;
|
|
12
|
+
/** Base pattern calculated from the path (e.g., "posts", "posts/:id", "" for global) */
|
|
13
|
+
basePattern: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Result of API directory scan
|
|
17
|
+
*/
|
|
18
|
+
export interface ScanResult {
|
|
19
|
+
routes: ParsedRoute[];
|
|
20
|
+
middlewares: MiddlewareInfo[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Recursively scans a directory looking for route files and middlewares
|
|
24
|
+
*/
|
|
25
|
+
export declare function scanApiDirectory(apiDir: string): ScanResult;
|
|
26
|
+
//# sourceMappingURL=scan-api-dir.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan-api-dir.d.ts","sourceRoot":"","sources":["../../../src/core/file-system/scan-api-dir.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAG9D;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,wFAAwF;IACxF,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,WAAW,EAAE,cAAc,EAAE,CAAC;CAC/B;AAsCD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAiD3D"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API directory scanner
|
|
3
|
+
* Core logic - uses Node.js fs but abstracted to be testable
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { ROUTE_FILE_PATTERN, MIDDLEWARE_FILE_PATTERN, PARAM_PATTERN } from '../../shared/constants.js';
|
|
8
|
+
import { parseRouteFile } from '../routing/route-parser.js';
|
|
9
|
+
/**
|
|
10
|
+
* Calculates the base pattern of a middleware based on the path relative to apiDir
|
|
11
|
+
* Example:
|
|
12
|
+
* - src/api/middleware.ts -> "" (global)
|
|
13
|
+
* - src/api/posts/middleware.ts -> "posts"
|
|
14
|
+
* - src/api/posts/[id]/middleware.ts -> "posts/:id"
|
|
15
|
+
*/
|
|
16
|
+
function calculateMiddlewareBasePattern(middlewarePath, apiDir) {
|
|
17
|
+
// Get the directory where the middleware is located
|
|
18
|
+
const middlewareDir = path.dirname(middlewarePath);
|
|
19
|
+
// Calculate relative path from middleware directory to apiDir
|
|
20
|
+
const relativePath = path.relative(apiDir, middlewareDir);
|
|
21
|
+
// If it's at the root (apiDir), it's a global middleware
|
|
22
|
+
if (!relativePath || relativePath === '.' || relativePath === '') {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
// Normalize separators
|
|
26
|
+
let pattern = relativePath.replace(/\\/g, '/');
|
|
27
|
+
// Remove leading/trailing slashes
|
|
28
|
+
pattern = pattern.replace(/^\/+|\/+$/g, '');
|
|
29
|
+
// Convert [id] to :id and [...ids] to *ids
|
|
30
|
+
pattern = pattern.replace(PARAM_PATTERN, (match, isCatchAll, paramName) => {
|
|
31
|
+
if (isCatchAll) {
|
|
32
|
+
return `*${paramName}`;
|
|
33
|
+
}
|
|
34
|
+
return `:${paramName}`;
|
|
35
|
+
});
|
|
36
|
+
return pattern;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Recursively scans a directory looking for route files and middlewares
|
|
40
|
+
*/
|
|
41
|
+
export function scanApiDirectory(apiDir) {
|
|
42
|
+
const routes = [];
|
|
43
|
+
const middlewares = [];
|
|
44
|
+
if (!fs.existsSync(apiDir)) {
|
|
45
|
+
return { routes, middlewares };
|
|
46
|
+
}
|
|
47
|
+
function scanDir(currentDir) {
|
|
48
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
scanDir(fullPath);
|
|
53
|
+
}
|
|
54
|
+
else if (entry.isFile()) {
|
|
55
|
+
// Check if it's a middleware file
|
|
56
|
+
if (MIDDLEWARE_FILE_PATTERN.test(entry.name)) {
|
|
57
|
+
const basePattern = calculateMiddlewareBasePattern(fullPath, apiDir);
|
|
58
|
+
middlewares.push({
|
|
59
|
+
path: fullPath,
|
|
60
|
+
basePattern,
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Check if it's a route file
|
|
65
|
+
if (ROUTE_FILE_PATTERN.test(entry.name)) {
|
|
66
|
+
const parsed = parseRouteFile(fullPath, apiDir);
|
|
67
|
+
if (parsed) {
|
|
68
|
+
routes.push(parsed);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
scanDir(apiDir);
|
|
75
|
+
// Sort middlewares by depth (most generic first, most specific last)
|
|
76
|
+
// This ensures that when composing, global middlewares come before specific ones
|
|
77
|
+
middlewares.sort((a, b) => {
|
|
78
|
+
const aDepth = a.basePattern ? a.basePattern.split('/').length : 0;
|
|
79
|
+
const bDepth = b.basePattern ? b.basePattern.split('/').length : 0;
|
|
80
|
+
return aDepth - bDepth;
|
|
81
|
+
});
|
|
82
|
+
return { routes, middlewares };
|
|
83
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API directory watcher
|
|
3
|
+
* Core logic - abstracted to be runtime agnostic
|
|
4
|
+
*/
|
|
5
|
+
export type FileChangeEvent = 'add' | 'change' | 'unlink';
|
|
6
|
+
export type FileChangeCallback = (event: FileChangeEvent, filePath: string) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Interface for watcher (allows different implementations)
|
|
9
|
+
*/
|
|
10
|
+
export interface ApiWatcher {
|
|
11
|
+
close(): void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates a watcher for the API directory using Node.js fs.watch
|
|
15
|
+
* Returns a function to stop watching
|
|
16
|
+
*/
|
|
17
|
+
export declare function watchApiDirectory(apiDir: string, callback: FileChangeCallback): ApiWatcher;
|
|
18
|
+
//# sourceMappingURL=watch-api-dir.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-api-dir.d.ts","sourceRoot":"","sources":["../../../src/core/file-system/watch-api-dir.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAC1D,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAEpF;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,kBAAkB,GAC3B,UAAU,CAiCZ"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API directory watcher
|
|
3
|
+
* Core logic - abstracted to be runtime agnostic
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { ROUTE_FILE_PATTERN, MIDDLEWARE_FILE_PATTERN } from '../../shared/constants.js';
|
|
8
|
+
/**
|
|
9
|
+
* Creates a watcher for the API directory using Node.js fs.watch
|
|
10
|
+
* Returns a function to stop watching
|
|
11
|
+
*/
|
|
12
|
+
export function watchApiDirectory(apiDir, callback) {
|
|
13
|
+
if (!fs.existsSync(apiDir)) {
|
|
14
|
+
return { close: () => { } };
|
|
15
|
+
}
|
|
16
|
+
const watcher = fs.watch(apiDir, { recursive: true }, (eventType, filename) => {
|
|
17
|
+
if (!filename)
|
|
18
|
+
return;
|
|
19
|
+
const filePath = path.join(apiDir, filename);
|
|
20
|
+
// Ignore if it's not a route file or relevant middleware
|
|
21
|
+
const isRouteFile = ROUTE_FILE_PATTERN.test(filename);
|
|
22
|
+
const isMiddlewareFile = MIDDLEWARE_FILE_PATTERN.test(filename);
|
|
23
|
+
if (!isRouteFile && !isMiddlewareFile) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Normalize event type
|
|
27
|
+
let normalizedEvent;
|
|
28
|
+
if (eventType === 'rename') {
|
|
29
|
+
// rename can be add or unlink, check if it exists
|
|
30
|
+
normalizedEvent = fs.existsSync(filePath) ? 'add' : 'unlink';
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
normalizedEvent = eventType;
|
|
34
|
+
}
|
|
35
|
+
callback(normalizedEvent, filePath);
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
close: () => watcher.close(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware composer (Koa-style)
|
|
3
|
+
* Core logic - no dependencies
|
|
4
|
+
*/
|
|
5
|
+
import type { Middleware } from '../routing/route-types.js';
|
|
6
|
+
import type { VitekContext } from '../context/create-context.js';
|
|
7
|
+
/**
|
|
8
|
+
* Composes middlewares into a single function
|
|
9
|
+
* Executes in order and allows each middleware to call next() to continue
|
|
10
|
+
*/
|
|
11
|
+
export declare function compose(middlewares: Middleware[]): (context: VitekContext, handler: () => Promise<any>) => Promise<any>;
|
|
12
|
+
//# sourceMappingURL=compose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../../src/core/middleware/compose.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEjE;;;GAGG;AACH,wBAAgB,OAAO,CAAC,WAAW,EAAE,UAAU,EAAE,IACxB,SAAS,YAAY,EAAE,SAAS,MAAM,OAAO,CAAC,GAAG,CAAC,KAAG,OAAO,CAAC,GAAG,CAAC,CAuBzF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware composer (Koa-style)
|
|
3
|
+
* Core logic - no dependencies
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Composes middlewares into a single function
|
|
7
|
+
* Executes in order and allows each middleware to call next() to continue
|
|
8
|
+
*/
|
|
9
|
+
export function compose(middlewares) {
|
|
10
|
+
return async function (context, handler) {
|
|
11
|
+
let index = -1;
|
|
12
|
+
async function dispatch(i) {
|
|
13
|
+
if (i <= index) {
|
|
14
|
+
throw new Error('next() called multiple times');
|
|
15
|
+
}
|
|
16
|
+
index = i;
|
|
17
|
+
// If all middlewares have been executed, call the final handler
|
|
18
|
+
if (i === middlewares.length) {
|
|
19
|
+
return handler();
|
|
20
|
+
}
|
|
21
|
+
const middleware = middlewares[i];
|
|
22
|
+
// Execute the current middleware
|
|
23
|
+
await middleware(context, () => dispatch(i + 1));
|
|
24
|
+
}
|
|
25
|
+
return dispatch(0);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines which middlewares are applicable to a route based on the pattern
|
|
3
|
+
* Core logic - no dependencies
|
|
4
|
+
*/
|
|
5
|
+
import type { Middleware } from '../routing/route-types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Information about a loaded middleware (with the middleware function already imported)
|
|
8
|
+
*/
|
|
9
|
+
export interface LoadedMiddleware {
|
|
10
|
+
middleware: Middleware[];
|
|
11
|
+
basePattern: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Returns the middlewares applicable to a route, ordered from most generic to most specific
|
|
15
|
+
*/
|
|
16
|
+
export declare function getApplicableMiddlewares(middlewares: LoadedMiddleware[], routePattern: string): Middleware[];
|
|
17
|
+
//# sourceMappingURL=get-applicable-middlewares.d.ts.map
|
|
@@ -0,0 +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;CACrB;AAuCD;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,gBAAgB,EAAE,EAC/B,YAAY,EAAE,MAAM,GACnB,UAAU,EAAE,CAUd"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines which middlewares are applicable to a route based on the pattern
|
|
3
|
+
* Core logic - no dependencies
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Checks if a middleware base pattern applies to a route pattern
|
|
7
|
+
* 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"
|
|
13
|
+
*/
|
|
14
|
+
function isMiddlewareApplicable(basePattern, routePattern) {
|
|
15
|
+
// Global middleware (empty basePattern) applies to all routes
|
|
16
|
+
if (basePattern === '') {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
// If route is empty (root), only global middleware applies
|
|
20
|
+
if (routePattern === '') {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
// Normalize patterns for comparison (remove leading/trailing slashes)
|
|
24
|
+
const normalizedBase = basePattern.replace(/^\/+|\/+$/g, '');
|
|
25
|
+
const normalizedRoute = routePattern.replace(/^\/+|\/+$/g, '');
|
|
26
|
+
// Check if basePattern is an exact prefix of routePattern
|
|
27
|
+
// Or if route starts with basePattern followed by /
|
|
28
|
+
if (normalizedRoute === normalizedBase) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (normalizedRoute.startsWith(normalizedBase + '/')) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns the middlewares applicable to a route, ordered from most generic to most specific
|
|
38
|
+
*/
|
|
39
|
+
export function getApplicableMiddlewares(middlewares, routePattern) {
|
|
40
|
+
const applicable = [];
|
|
41
|
+
for (const loadedMiddleware of middlewares) {
|
|
42
|
+
if (isMiddlewareApplicable(loadedMiddleware.basePattern, routePattern)) {
|
|
43
|
+
applicable.push(...loadedMiddleware.middleware);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return applicable;
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global middleware loading
|
|
3
|
+
* Core logic - runtime agnostic
|
|
4
|
+
*/
|
|
5
|
+
import type { Middleware } from '../routing/route-types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Loads global middleware from a module
|
|
8
|
+
* The module must export a default or named export 'middleware'
|
|
9
|
+
*
|
|
10
|
+
* @param middlewarePath - Absolute path to the middleware file
|
|
11
|
+
*/
|
|
12
|
+
export declare function loadGlobalMiddleware(middlewarePath: string): Promise<Middleware[]>;
|
|
13
|
+
//# sourceMappingURL=load-global.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-global.d.ts","sourceRoot":"","sources":["../../../src/core/middleware/load-global.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CA8BxF"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global middleware loading
|
|
3
|
+
* Core logic - runtime agnostic
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Loads global middleware from a module
|
|
7
|
+
* The module must export a default or named export 'middleware'
|
|
8
|
+
*
|
|
9
|
+
* @param middlewarePath - Absolute path to the middleware file
|
|
10
|
+
*/
|
|
11
|
+
export async function loadGlobalMiddleware(middlewarePath) {
|
|
12
|
+
try {
|
|
13
|
+
// Dynamic import - converts to URL for Vite/Node compatibility
|
|
14
|
+
const { pathToFileURL } = await import('node:url');
|
|
15
|
+
const moduleUrl = pathToFileURL(middlewarePath).href;
|
|
16
|
+
const module = await import(moduleUrl);
|
|
17
|
+
// Supports both default export and named export
|
|
18
|
+
const middleware = module.default || module.middleware;
|
|
19
|
+
if (!middleware) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
// If it's an array, return directly
|
|
23
|
+
if (Array.isArray(middleware)) {
|
|
24
|
+
return middleware;
|
|
25
|
+
}
|
|
26
|
+
// If it's a single function, return as array
|
|
27
|
+
if (typeof middleware === 'function') {
|
|
28
|
+
return [middleware];
|
|
29
|
+
}
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// If file doesn't exist or has no middleware, return empty array
|
|
34
|
+
// This allows middleware to be optional
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path normalization for API routes
|
|
3
|
+
* Core logic - no Vite dependencies
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Converts a file path to a normalized HTTP route
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* - users/[id].get.ts -> users/:id
|
|
10
|
+
* - posts/[...ids].get.ts -> posts/*ids
|
|
11
|
+
* - health.get.ts -> health
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeRoutePath(filePath: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Extracts parameters from a path pattern
|
|
16
|
+
* Example: "users/:id/posts/:postId" -> ["id", "postId"]
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractParamsFromPattern(pattern: string): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Converts a path pattern to regex for matching
|
|
21
|
+
* Example: "users/:id" -> /^\/users\/([^/]+)$/
|
|
22
|
+
* Example: "posts/*ids" -> /^\/posts\/(.*)$/
|
|
23
|
+
*/
|
|
24
|
+
export declare function patternToRegex(pattern: string): RegExp;
|
|
25
|
+
//# sourceMappingURL=normalize-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize-path.d.ts","sourceRoot":"","sources":["../../../src/core/normalize/normalize-path.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA2B3D;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAUlE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0BtD"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path normalization for API routes
|
|
3
|
+
* Core logic - no Vite dependencies
|
|
4
|
+
*/
|
|
5
|
+
import { PARAM_PATTERN } from '../../shared/constants.js';
|
|
6
|
+
/**
|
|
7
|
+
* Converts a file path to a normalized HTTP route
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* - users/[id].get.ts -> users/:id
|
|
11
|
+
* - posts/[...ids].get.ts -> posts/*ids
|
|
12
|
+
* - health.get.ts -> health
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeRoutePath(filePath) {
|
|
15
|
+
// Remove extensions (.ts, .js) and HTTP method
|
|
16
|
+
let path = filePath
|
|
17
|
+
.replace(/\.(ts|js)$/, '')
|
|
18
|
+
.replace(/\.(get|post|put|patch|delete|head|options)$/, '');
|
|
19
|
+
// Normalize separators
|
|
20
|
+
path = path.replace(/\\/g, '/');
|
|
21
|
+
// Remove leading/trailing slashes (but preserve empty string for root route)
|
|
22
|
+
path = path.replace(/^\/+|\/+$/g, '');
|
|
23
|
+
// If path is empty, it means root route (e.g., health.get.ts)
|
|
24
|
+
if (!path) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
// Convert [id] to :id and [...ids] to *ids (catch-all)
|
|
28
|
+
path = path.replace(PARAM_PATTERN, (match, isCatchAll, paramName) => {
|
|
29
|
+
if (isCatchAll) {
|
|
30
|
+
return `*${paramName}`;
|
|
31
|
+
}
|
|
32
|
+
return `:${paramName}`;
|
|
33
|
+
});
|
|
34
|
+
// Return without normalizing (without adding /), as it will be used in patternToRegex
|
|
35
|
+
return path;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Extracts parameters from a path pattern
|
|
39
|
+
* Example: "users/:id/posts/:postId" -> ["id", "postId"]
|
|
40
|
+
*/
|
|
41
|
+
export function extractParamsFromPattern(pattern) {
|
|
42
|
+
const params = [];
|
|
43
|
+
const paramRegex = /[:*]([^/]+)/g;
|
|
44
|
+
let match;
|
|
45
|
+
while ((match = paramRegex.exec(pattern)) !== null) {
|
|
46
|
+
params.push(match[1]);
|
|
47
|
+
}
|
|
48
|
+
return params;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Converts a path pattern to regex for matching
|
|
52
|
+
* Example: "users/:id" -> /^\/users\/([^/]+)$/
|
|
53
|
+
* Example: "posts/*ids" -> /^\/posts\/(.*)$/
|
|
54
|
+
*/
|
|
55
|
+
export function patternToRegex(pattern) {
|
|
56
|
+
// Normalize pattern: empty route becomes '/', others add / at the beginning
|
|
57
|
+
let normalizedPattern = pattern === '' ? '/' : (pattern.startsWith('/') ? pattern : `/${pattern}`);
|
|
58
|
+
// First replace placeholders (before escaping)
|
|
59
|
+
// Replace *param (catch-all) with temporary placeholder
|
|
60
|
+
normalizedPattern = normalizedPattern.replace(/\*(\w+)/g, '__CATCHALL_$1__');
|
|
61
|
+
// Replace :param with temporary placeholder
|
|
62
|
+
normalizedPattern = normalizedPattern.replace(/:(\w+)/g, '__PARAM_$1__');
|
|
63
|
+
// Escape special characters
|
|
64
|
+
let regexStr = normalizedPattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
65
|
+
// Restore placeholders as capture groups
|
|
66
|
+
regexStr = regexStr.replace(/__PARAM_\w+__/g, '([^/]+)');
|
|
67
|
+
regexStr = regexStr.replace(/__CATCHALL_(\w+)__/g, '(.*)');
|
|
68
|
+
// Ensure it starts and ends correctly
|
|
69
|
+
if (!regexStr.startsWith('^')) {
|
|
70
|
+
regexStr = '^' + regexStr;
|
|
71
|
+
}
|
|
72
|
+
if (!regexStr.endsWith('$')) {
|
|
73
|
+
regexStr = regexStr + '$';
|
|
74
|
+
}
|
|
75
|
+
return new RegExp(regexStr);
|
|
76
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route matching against requests
|
|
3
|
+
* Core logic - no Vite dependencies
|
|
4
|
+
*/
|
|
5
|
+
import type { Route, RouteMatch } from './route-types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Finds the route that matches a path and method
|
|
8
|
+
*/
|
|
9
|
+
export declare function matchRoute(routes: Route[], path: string, method: string): RouteMatch | null;
|
|
10
|
+
//# sourceMappingURL=route-matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../../src/core/routing/route-matcher.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG1D;;GAEG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,KAAK,EAAE,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,UAAU,GAAG,IAAI,CA4BnB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route matching against requests
|
|
3
|
+
* Core logic - no Vite dependencies
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Finds the route that matches a path and method
|
|
7
|
+
*/
|
|
8
|
+
export function matchRoute(routes, path, method) {
|
|
9
|
+
// Normalize path ensuring it starts with /
|
|
10
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
11
|
+
const normalizedMethod = method.toLowerCase();
|
|
12
|
+
// Filter routes by method
|
|
13
|
+
const methodRoutes = routes.filter(r => r.method === normalizedMethod);
|
|
14
|
+
// Try to match in order
|
|
15
|
+
for (const route of methodRoutes) {
|
|
16
|
+
const match = normalizedPath.match(route.regex);
|
|
17
|
+
if (match) {
|
|
18
|
+
// Extract parameters from match
|
|
19
|
+
const params = {};
|
|
20
|
+
route.params.forEach((paramName, index) => {
|
|
21
|
+
// +1 because match[0] is the complete match
|
|
22
|
+
const value = match[index + 1];
|
|
23
|
+
params[paramName] = value !== undefined ? value : '';
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
route,
|
|
27
|
+
params,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route file parser
|
|
3
|
+
* Core logic - no Vite dependencies
|
|
4
|
+
*/
|
|
5
|
+
import type { Route, RouteHandler } from './route-types.js';
|
|
6
|
+
import type { HttpMethod } from '../../shared/constants.js';
|
|
7
|
+
/**
|
|
8
|
+
* Result of parsing a route file
|
|
9
|
+
*/
|
|
10
|
+
export interface ParsedRoute {
|
|
11
|
+
method: HttpMethod;
|
|
12
|
+
pattern: string;
|
|
13
|
+
params: string[];
|
|
14
|
+
file: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extracts route information from a file path
|
|
18
|
+
*
|
|
19
|
+
* Examples:
|
|
20
|
+
* - src/api/users/[id].get.ts -> { method: 'get', pattern: 'users/:id', params: ['id'] }
|
|
21
|
+
* - src/api/posts/[...ids].get.ts -> { method: 'get', pattern: 'posts/*ids', params: ['ids'] }
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseRouteFile(filePath: string, baseDir: string): ParsedRoute | null;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a route definition from a parsed route and handler
|
|
26
|
+
*/
|
|
27
|
+
export declare function createRoute(parsed: ParsedRoute, handler: RouteHandler, bodyType?: string, queryType?: string): Route;
|
|
28
|
+
//# sourceMappingURL=route-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-parser.d.ts","sourceRoot":"","sources":["../../../src/core/routing/route-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CA2BpF;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAWpH"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route file parser
|
|
3
|
+
* Core logic - no Vite dependencies
|
|
4
|
+
*/
|
|
5
|
+
import { ROUTE_FILE_PATTERN } from '../../shared/constants.js';
|
|
6
|
+
import { isHttpMethod } from '../../shared/utils.js';
|
|
7
|
+
import { normalizeRoutePath, extractParamsFromPattern, patternToRegex } from '../normalize/normalize-path.js';
|
|
8
|
+
/**
|
|
9
|
+
* Extracts route information from a file path
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* - src/api/users/[id].get.ts -> { method: 'get', pattern: 'users/:id', params: ['id'] }
|
|
13
|
+
* - src/api/posts/[...ids].get.ts -> { method: 'get', pattern: 'posts/*ids', params: ['ids'] }
|
|
14
|
+
*/
|
|
15
|
+
export function parseRouteFile(filePath, baseDir) {
|
|
16
|
+
// Remove base directory from path
|
|
17
|
+
const relativePath = filePath.replace(baseDir, '').replace(/^[/\\]/, '');
|
|
18
|
+
// Try to match route file pattern
|
|
19
|
+
const match = relativePath.match(ROUTE_FILE_PATTERN);
|
|
20
|
+
if (!match) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const [, pathPart, methodPart] = match;
|
|
24
|
+
const method = methodPart.toLowerCase();
|
|
25
|
+
if (!isHttpMethod(method)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
// Normalize path and extract parameters
|
|
29
|
+
const pattern = normalizeRoutePath(pathPart);
|
|
30
|
+
const params = extractParamsFromPattern(pattern);
|
|
31
|
+
return {
|
|
32
|
+
method,
|
|
33
|
+
pattern,
|
|
34
|
+
params,
|
|
35
|
+
file: filePath,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Creates a route definition from a parsed route and handler
|
|
40
|
+
*/
|
|
41
|
+
export function createRoute(parsed, handler, bodyType, queryType) {
|
|
42
|
+
return {
|
|
43
|
+
pattern: parsed.pattern,
|
|
44
|
+
method: parsed.method,
|
|
45
|
+
handler,
|
|
46
|
+
params: parsed.params,
|
|
47
|
+
file: parsed.file,
|
|
48
|
+
regex: patternToRegex(parsed.pattern),
|
|
49
|
+
bodyType,
|
|
50
|
+
queryType,
|
|
51
|
+
};
|
|
52
|
+
}
|