@epoch32/visage 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +110 -2
  3. package/dist/bundle.dev.js +4875 -0
  4. package/dist/index.js +9633 -0
  5. package/dist/src/api/index.js +4002 -0
  6. package/dist/src/charts/index.js +2717 -0
  7. package/dist/src/devtools/index.js +4677 -0
  8. package/dist/src/dom/index.js +1550 -0
  9. package/dist/src/form/index.js +1415 -0
  10. package/dist/src/index.js +11826 -0
  11. package/dist/src/jsx-runtime/index.js +1630 -0
  12. package/dist/src/query/index.js +2422 -0
  13. package/dist/src/router/index.js +1856 -0
  14. package/dist/src/rpc/index.js +3484 -0
  15. package/dist/src/server/index.js +1173 -0
  16. package/dist/src/signals/index.js +1077 -0
  17. package/dist/src/style/index.js +2340 -0
  18. package/dist/src/template/index.js +1149 -0
  19. package/dist/src/ui/index.js +4155 -0
  20. package/dist/src/virtual/index.js +1955 -0
  21. package/dist/src/vite/index.js +984 -0
  22. package/dist/vite/index.js +707 -0
  23. package/package.json +140 -4
  24. package/src/api/client.ts +371 -0
  25. package/src/api/file-router.ts +183 -0
  26. package/src/api/handler.ts +326 -0
  27. package/src/api/index.ts +71 -0
  28. package/src/api/middleware.ts +54 -0
  29. package/src/api/pragma.ts +155 -0
  30. package/src/api/procedure.ts +172 -0
  31. package/src/api/types.ts +249 -0
  32. package/src/charts/Area.tsx +111 -0
  33. package/src/charts/Axis.tsx +126 -0
  34. package/src/charts/Bar.tsx +86 -0
  35. package/src/charts/CartesianGrid.tsx +72 -0
  36. package/src/charts/Chart.tsx +269 -0
  37. package/src/charts/Cursor.tsx +42 -0
  38. package/src/charts/Legend.tsx +56 -0
  39. package/src/charts/Line.tsx +106 -0
  40. package/src/charts/Pie.tsx +88 -0
  41. package/src/charts/Tooltip.tsx +127 -0
  42. package/src/charts/context.ts +30 -0
  43. package/src/charts/index.ts +14 -0
  44. package/src/charts/path.ts +155 -0
  45. package/src/charts/scale.ts +133 -0
  46. package/src/charts/types.ts +50 -0
  47. package/src/devtools/DomTab.tsx +134 -0
  48. package/src/devtools/GraphInspector.tsx +196 -0
  49. package/src/devtools/GraphTab.tsx +315 -0
  50. package/src/devtools/QueryTab.tsx +200 -0
  51. package/src/devtools/RpcTab.tsx +190 -0
  52. package/src/devtools/SignalsTab.tsx +129 -0
  53. package/src/devtools/component.tsx +243 -0
  54. package/src/devtools/highlighter.ts +134 -0
  55. package/src/devtools/hub.ts +175 -0
  56. package/src/devtools/index.ts +20 -0
  57. package/src/devtools/styles.ts +313 -0
  58. package/src/devtools/types.ts +45 -0
  59. package/src/dom/async.ts +232 -0
  60. package/src/dom/control-flow.ts +353 -0
  61. package/src/dom/index.ts +32 -0
  62. package/src/dom/lazy.ts +82 -0
  63. package/src/dom/render.ts +286 -0
  64. package/src/dom/resource.ts +120 -0
  65. package/src/form/form.ts +218 -0
  66. package/src/form/index.ts +17 -0
  67. package/src/form/schema.ts +299 -0
  68. package/src/index.ts +413 -0
  69. package/src/jsx-dev-runtime/index.ts +1 -0
  70. package/src/jsx-runtime/index.ts +112 -0
  71. package/src/jsx-runtime/jsx.d.ts +278 -0
  72. package/src/query/create-mutation.ts +64 -0
  73. package/src/query/create-query.ts +426 -0
  74. package/src/query/focus-manager.ts +68 -0
  75. package/src/query/index.ts +63 -0
  76. package/src/query/mutation-cache.ts +138 -0
  77. package/src/query/online-manager.ts +67 -0
  78. package/src/query/query-cache.ts +369 -0
  79. package/src/query/query-client.ts +248 -0
  80. package/src/query/types.ts +221 -0
  81. package/src/query/utils.ts +132 -0
  82. package/src/router/index.ts +19 -0
  83. package/src/router/router.tsx +352 -0
  84. package/src/rpc/client.ts +317 -0
  85. package/src/rpc/error.ts +70 -0
  86. package/src/rpc/index.ts +52 -0
  87. package/src/rpc/procedure.ts +88 -0
  88. package/src/rpc/router.ts +105 -0
  89. package/src/rpc/server.ts +511 -0
  90. package/src/rpc/types.ts +328 -0
  91. package/src/server/hydrate.ts +21 -0
  92. package/src/server/index.ts +10 -0
  93. package/src/server/render-to-stream.ts +31 -0
  94. package/src/server/render-to-string.ts +125 -0
  95. package/src/signals/computed.ts +217 -0
  96. package/src/signals/context.ts +89 -0
  97. package/src/signals/effect.ts +161 -0
  98. package/src/signals/index.ts +57 -0
  99. package/src/signals/scope.ts +132 -0
  100. package/src/signals/signal.ts +203 -0
  101. package/src/signals/store.ts +163 -0
  102. package/src/signals/system.ts +124 -0
  103. package/src/signals/types.ts +64 -0
  104. package/src/style/component.tsx +122 -0
  105. package/src/style/create-style.ts +229 -0
  106. package/src/style/cx.ts +75 -0
  107. package/src/style/hash.ts +19 -0
  108. package/src/style/index.ts +8 -0
  109. package/src/style/layout.tsx +406 -0
  110. package/src/style/manager.ts +87 -0
  111. package/src/style/parser.ts +257 -0
  112. package/src/style/scale.ts +237 -0
  113. package/src/template/index.ts +4 -0
  114. package/src/template/template.ts +126 -0
  115. package/src/ui/accordion.tsx +286 -0
  116. package/src/ui/command-dialog.tsx +49 -0
  117. package/src/ui/command.tsx +400 -0
  118. package/src/ui/data-table.tsx +322 -0
  119. package/src/ui/dialog.tsx +362 -0
  120. package/src/ui/dropdown-menu.tsx +381 -0
  121. package/src/ui/icon.tsx +69 -0
  122. package/src/ui/index.ts +138 -0
  123. package/src/ui/layout.tsx +13 -0
  124. package/src/ui/popover.tsx +249 -0
  125. package/src/ui/switch.tsx +114 -0
  126. package/src/ui/tabs.tsx +297 -0
  127. package/src/ui/toast.tsx +280 -0
  128. package/src/ui/tooltip.tsx +262 -0
  129. package/src/ui/utils.ts +110 -0
  130. package/src/virtual/index.ts +12 -0
  131. package/src/virtual/virtual-list.tsx +185 -0
  132. package/src/virtual/virtualizer.ts +274 -0
  133. package/src/vite/hmr.ts +38 -0
  134. package/src/vite/index.ts +3 -0
  135. package/src/vite/lucide.ts +124 -0
  136. package/src/vite/plugin.ts +102 -0
  137. package/src/vite/rpc-middleware.ts +82 -0
  138. package/src/vite/types.ts +65 -0
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Visage Web Standard API Fetch Handler & Request Dispatcher
3
+ */
4
+ import { validateWithSchema } from "../form/schema";
5
+ import { ERROR_CODE_TO_STATUS, RPCError } from "../rpc/error";
6
+ import { buildFileRouter } from "./file-router";
7
+ import { executeMiddlewareChain } from "./middleware";
8
+ import type { ApiProcedure, ApiRouterConfig, FileRouteModule, RouteMatch } from "./types";
9
+
10
+ /**
11
+ * Handle CORS preflight & headers
12
+ */
13
+ function handleCors(req: Request, config?: ApiRouterConfig["cors"]): Response | null {
14
+ if (!config) return null;
15
+
16
+ const origin = req.headers.get("Origin") || "*";
17
+ const isAllowed =
18
+ !config.origin ||
19
+ config.origin === "*" ||
20
+ (typeof config.origin === "function" && config.origin(origin)) ||
21
+ (Array.isArray(config.origin) && config.origin.includes(origin)) ||
22
+ config.origin === origin;
23
+
24
+ if (req.method === "OPTIONS") {
25
+ const headers = new Headers();
26
+ if (isAllowed) {
27
+ headers.set("Access-Control-Allow-Origin", origin);
28
+ }
29
+ headers.set(
30
+ "Access-Control-Allow-Methods",
31
+ (config.methods || ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]).join(", ")
32
+ );
33
+ headers.set(
34
+ "Access-Control-Allow-Headers",
35
+ (config.allowedHeaders || ["Content-Type", "Authorization", "x-visage-rpc"]).join(", ")
36
+ );
37
+ if (config.credentials) {
38
+ headers.set("Access-Control-Allow-Credentials", "true");
39
+ }
40
+ if (config.maxAge) {
41
+ headers.set("Access-Control-Max-Age", config.maxAge.toString());
42
+ }
43
+ return new Response(null, { status: 204, headers });
44
+ }
45
+
46
+ return null;
47
+ }
48
+
49
+ function applyCorsHeaders(res: Response, req: Request, config?: ApiRouterConfig["cors"]): Response {
50
+ if (!config) return res;
51
+ const origin = req.headers.get("Origin") || "*";
52
+ res.headers.set("Access-Control-Allow-Origin", origin);
53
+ if (config.credentials) {
54
+ res.headers.set("Access-Control-Allow-Credentials", "true");
55
+ }
56
+ return res;
57
+ }
58
+
59
+ /**
60
+ * Create a standard Web Fetch API handler from compiled route manifests
61
+ */
62
+ export function createApiHandler(
63
+ routes: RouteMatch[],
64
+ config: ApiRouterConfig = {}
65
+ ): (req: Request) => Promise<Response> {
66
+ const prefix = config.prefix ? `/${config.prefix.replace(/^\/+|\/+$/g, "")}` : "";
67
+
68
+ return async (req: Request): Promise<Response> => {
69
+ // 1. CORS Preflight Check
70
+ const corsPreflight = handleCors(req, config.cors);
71
+ if (corsPreflight) return corsPreflight;
72
+
73
+ const url = new URL(req.url);
74
+ let pathname = url.pathname;
75
+
76
+ if (prefix && pathname.startsWith(prefix)) {
77
+ pathname = pathname.slice(prefix.length) || "/";
78
+ }
79
+
80
+ // 2. Route Matching
81
+ let matchedRoute: RouteMatch | null = null;
82
+ const pathParams: Record<string, string> = {};
83
+
84
+ for (const route of routes) {
85
+ const match = pathname.match(route.pattern);
86
+ if (match) {
87
+ matchedRoute = route;
88
+ route.paramNames.forEach((name, i) => {
89
+ pathParams[name] = decodeURIComponent(match[i + 1] || "");
90
+ });
91
+ break;
92
+ }
93
+ }
94
+
95
+ if (!matchedRoute) {
96
+ return applyCorsHeaders(
97
+ new Response(
98
+ JSON.stringify({
99
+ ok: false,
100
+ error: { code: "NOT_FOUND", message: `Route not found: ${pathname}` }
101
+ }),
102
+ { status: 404, headers: { "Content-Type": "application/json" } }
103
+ ),
104
+ req,
105
+ config.cors
106
+ );
107
+ }
108
+
109
+ try {
110
+ const method = req.method.toUpperCase();
111
+ let procedure: ApiProcedure | undefined = matchedRoute.procedures.default;
112
+
113
+ if (!procedure) {
114
+ if (method === "GET") {
115
+ procedure = matchedRoute.procedures.GET || matchedRoute.procedures.get;
116
+ } else if (method === "POST") {
117
+ procedure = matchedRoute.procedures.POST || matchedRoute.procedures.post;
118
+ } else if (method === "PUT") {
119
+ procedure = matchedRoute.procedures.PUT || matchedRoute.procedures.put;
120
+ } else if (method === "DELETE") {
121
+ procedure =
122
+ matchedRoute.procedures.DELETE ||
123
+ matchedRoute.procedures.del ||
124
+ matchedRoute.procedures.delete;
125
+ } else if (method === "PATCH") {
126
+ procedure = matchedRoute.procedures.PATCH || matchedRoute.procedures.patch;
127
+ }
128
+ }
129
+
130
+ // 3. Execute Middleware Chain (File-level + Procedure-level)
131
+ const mwFns = [
132
+ ...matchedRoute.middlewares.map((m) => m.fn),
133
+ ...(procedure?.middlewares || [])
134
+ ];
135
+
136
+ const { response: earlyResponse, ctx } = await executeMiddlewareChain(
137
+ mwFns,
138
+ req,
139
+ url,
140
+ pathParams
141
+ );
142
+
143
+ if (earlyResponse) {
144
+ return applyCorsHeaders(earlyResponse, req, config.cors);
145
+ }
146
+
147
+ // 4. Custom Raw Handler
148
+ if (matchedRoute.handler) {
149
+ const res = await matchedRoute.handler(req, ctx);
150
+ return applyCorsHeaders(res, req, config.cors);
151
+ }
152
+
153
+ if (!procedure) {
154
+ return applyCorsHeaders(
155
+ new Response(
156
+ JSON.stringify({
157
+ ok: false,
158
+ error: {
159
+ code: "METHOD_NOT_ALLOWED",
160
+ message: `Method ${method} not allowed on ${pathname}`
161
+ }
162
+ }),
163
+ { status: 405, headers: { "Content-Type": "application/json" } }
164
+ ),
165
+ req,
166
+ config.cors
167
+ );
168
+ }
169
+
170
+ // Parse Input
171
+ let rawInput: any = {};
172
+ if (method === "GET") {
173
+ const inputQuery = url.searchParams.get("input");
174
+ if (inputQuery) {
175
+ try {
176
+ rawInput = JSON.parse(inputQuery);
177
+ } catch {
178
+ rawInput = inputQuery;
179
+ }
180
+ } else {
181
+ // Flatten searchParams
182
+ const spObj: Record<string, string> = {};
183
+ url.searchParams.forEach((val, key) => {
184
+ spObj[key] = val;
185
+ });
186
+ if (Object.keys(spObj).length > 0) {
187
+ rawInput = spObj;
188
+ }
189
+ }
190
+ } else {
191
+ const contentType = req.headers.get("Content-Type") || "";
192
+ if (contentType.includes("application/json")) {
193
+ try {
194
+ rawInput = await req.json();
195
+ } catch {
196
+ rawInput = {};
197
+ }
198
+ }
199
+ }
200
+
201
+ // Merge Path Params into Input if object
202
+ if (typeof rawInput === "object" && rawInput !== null) {
203
+ rawInput = { ...pathParams, ...rawInput };
204
+ }
205
+
206
+ // Validate Input Schema
207
+ let validatedInput = rawInput;
208
+ if (procedure.options.input) {
209
+ const validation = await validateWithSchema(procedure.options.input, rawInput);
210
+ if (!validation.success) {
211
+ return applyCorsHeaders(
212
+ new Response(
213
+ JSON.stringify({
214
+ ok: false,
215
+ error: {
216
+ code: "BAD_REQUEST",
217
+ message: "Validation Error",
218
+ issues: validation.issues
219
+ }
220
+ }),
221
+ { status: 400, headers: { "Content-Type": "application/json" } }
222
+ ),
223
+ req,
224
+ config.cors
225
+ );
226
+ }
227
+ validatedInput = validation.data;
228
+ }
229
+
230
+ // Run Procedure
231
+ if (procedure.type === "subscription") {
232
+ // SSE Real-time Streaming
233
+ const generator = procedure.options.resolve({
234
+ input: validatedInput,
235
+ ctx,
236
+ req,
237
+ params: pathParams
238
+ }) as AsyncGenerator<any, void, unknown>;
239
+
240
+ const stream = new ReadableStream({
241
+ async start(controller) {
242
+ const encoder = new TextEncoder();
243
+ try {
244
+ for await (const chunk of generator) {
245
+ const payload = `data: ${JSON.stringify(chunk)}\n\n`;
246
+ controller.enqueue(encoder.encode(payload));
247
+ }
248
+ controller.close();
249
+ } catch (err: any) {
250
+ controller.error(err);
251
+ }
252
+ }
253
+ });
254
+
255
+ const sseRes = new Response(stream, {
256
+ headers: {
257
+ "Content-Type": "text/event-stream",
258
+ "Cache-Control": "no-cache",
259
+ Connection: "keep-alive"
260
+ }
261
+ });
262
+ return applyCorsHeaders(sseRes, req, config.cors);
263
+ }
264
+
265
+ const result = await procedure.options.resolve({
266
+ input: validatedInput,
267
+ ctx,
268
+ req,
269
+ params: pathParams
270
+ });
271
+
272
+ const successRes = new Response(JSON.stringify({ ok: true, data: result }), {
273
+ status: 200,
274
+ headers: { "Content-Type": "application/json" }
275
+ });
276
+ return applyCorsHeaders(successRes, req, config.cors);
277
+ } catch (err: any) {
278
+ if (config.onError) {
279
+ const customRes = await config.onError(err, req);
280
+ return applyCorsHeaders(customRes, req, config.cors);
281
+ }
282
+
283
+ const statusCode =
284
+ err instanceof RPCError
285
+ ? err.statusCode
286
+ : ERROR_CODE_TO_STATUS[err?.code as keyof typeof ERROR_CODE_TO_STATUS] || 500;
287
+
288
+ const code = err instanceof RPCError ? err.code : "INTERNAL_SERVER_ERROR";
289
+ const message = err?.message || "Internal Server Error";
290
+
291
+ const errRes = new Response(JSON.stringify({ ok: false, error: { code, message } }), {
292
+ status: statusCode,
293
+ headers: { "Content-Type": "application/json" }
294
+ });
295
+ return applyCorsHeaders(errRes, req, config.cors);
296
+ }
297
+ };
298
+ }
299
+
300
+ /**
301
+ * High-level createApi factory for file-based API routers
302
+ */
303
+ export function createApi(
304
+ files: Record<string, FileRouteModule | { default?: any; [key: string]: any }>,
305
+ sourceMapOrConfig?: Record<string, string> | ApiRouterConfig,
306
+ maybeConfig?: ApiRouterConfig
307
+ ) {
308
+ const sourceMap =
309
+ sourceMapOrConfig && !("prefix" in sourceMapOrConfig) && !("cors" in sourceMapOrConfig)
310
+ ? (sourceMapOrConfig as Record<string, string>)
311
+ : {};
312
+
313
+ const config =
314
+ maybeConfig ||
315
+ (sourceMapOrConfig && ("prefix" in sourceMapOrConfig || "cors" in sourceMapOrConfig)
316
+ ? (sourceMapOrConfig as ApiRouterConfig)
317
+ : {});
318
+
319
+ const routes = buildFileRouter(files, sourceMap);
320
+ const fetchHandler = createApiHandler(routes, config);
321
+
322
+ return {
323
+ routes,
324
+ fetch: fetchHandler
325
+ };
326
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Visage File-Based API & Opinionated RPC Framework (visage/api)
3
+ */
4
+
5
+ // Re-export standard schema validation & RPC errors
6
+ export {
7
+ Schema,
8
+ type SchemaLike,
9
+ type StandardSchemaV1,
10
+ type ValidationIssue,
11
+ type ValidationResult,
12
+ v,
13
+ validateWithSchema
14
+ } from "../form/schema";
15
+ export {
16
+ ERROR_CODE_TO_STATUS,
17
+ RPCError,
18
+ STATUS_TO_ERROR_CODE
19
+ } from "../rpc/error";
20
+ export { createApiClient } from "./client";
21
+ export {
22
+ buildFileRouter,
23
+ compileRoutePattern,
24
+ type FileRouteEntry,
25
+ normalizeFilePath
26
+ } from "./file-router";
27
+ export {
28
+ createApi,
29
+ createApiHandler
30
+ } from "./handler";
31
+ export { executeMiddlewareChain } from "./middleware";
32
+ export {
33
+ globToRegExp,
34
+ matchesAnyPattern,
35
+ parsePragma,
36
+ shouldApplyMiddleware
37
+ } from "./pragma";
38
+ export {
39
+ defineMiddleware,
40
+ mutation,
41
+ ProcedureBuilder,
42
+ procedure,
43
+ query,
44
+ subscription
45
+ } from "./procedure";
46
+ export type {
47
+ ApiCallOptions,
48
+ ApiClient,
49
+ ApiClientHeadersRule,
50
+ ApiClientOptions,
51
+ ApiEndpointMethods,
52
+ ApiMethodCaller,
53
+ ApiProcedure,
54
+ ApiRouterConfig,
55
+ ApiSubscriptionCallbacks,
56
+ ApiSubscriptionCaller,
57
+ ApiSubscriptionHandle,
58
+ FileRouteModule,
59
+ HttpMethod,
60
+ InferApiInput,
61
+ InferApiOutput,
62
+ MiddlewareContext,
63
+ MiddlewareDefinition,
64
+ MiddlewareFn,
65
+ MiddlewarePragma,
66
+ NextFn,
67
+ ProcedureOptions,
68
+ ProcedureType,
69
+ RouteMatch,
70
+ RPCErrorCode
71
+ } from "./types";
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Visage Middleware Execution Engine & Context Propagation
3
+ */
4
+ import type { MiddlewareFn, NextFn } from "./types";
5
+
6
+ /**
7
+ * Execute a chain of middlewares, accumulating context and returning either an early Response or merged Context
8
+ */
9
+ export async function executeMiddlewareChain(
10
+ middlewares: MiddlewareFn[],
11
+ req: Request,
12
+ url: URL,
13
+ params: Record<string, string>,
14
+ initialCtx: Record<string, any> = {}
15
+ ): Promise<{ response?: Response; ctx: Record<string, any> }> {
16
+ let currentCtx = { ...initialCtx };
17
+ let earlyResponse: Response | undefined;
18
+
19
+ let index = 0;
20
+
21
+ const runner = async (): Promise<void> => {
22
+ if (index >= middlewares.length) {
23
+ return;
24
+ }
25
+
26
+ const currentMw = middlewares[index++]!;
27
+ const next: NextFn = async (enriched) => {
28
+ if (enriched && typeof enriched === "object") {
29
+ currentCtx = { ...currentCtx, ...enriched };
30
+ }
31
+ await runner();
32
+ return currentCtx;
33
+ };
34
+
35
+ const result = await currentMw({
36
+ req,
37
+ ctx: currentCtx,
38
+ url,
39
+ params,
40
+ next
41
+ });
42
+
43
+ if (result instanceof Response) {
44
+ earlyResponse = result;
45
+ }
46
+ };
47
+
48
+ await runner();
49
+
50
+ return {
51
+ response: earlyResponse,
52
+ ctx: currentCtx
53
+ };
54
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Visage API Pragma Parser & Path Pattern Matcher
3
+ * Parses `// @visage-middleware` pragmas at the top of middleware files.
4
+ */
5
+ import type { MiddlewarePragma } from "./types";
6
+
7
+ const PRAGMA_REGEX =
8
+ /(?:\/\/\s*@visage-middleware\s+([^\r\n]+))|(?:\/\*\s*@visage-middleware\s+([\s\S]*?)\*\/)/i;
9
+
10
+ /**
11
+ * Parse top-level `// @visage-middleware` pragma from file source code
12
+ */
13
+ export function parsePragma(sourceCode: string): MiddlewarePragma {
14
+ if (!sourceCode || typeof sourceCode !== "string") {
15
+ return { cascade: true, match: ["*"], exclude: [], priority: 0 };
16
+ }
17
+
18
+ const match = sourceCode.match(PRAGMA_REGEX);
19
+ if (!match) {
20
+ return { cascade: true, match: ["*"], exclude: [], priority: 0 };
21
+ }
22
+
23
+ const rawDirective = (match[1] || match[2] || "").trim();
24
+ const pragma: MiddlewarePragma = {
25
+ cascade: true,
26
+ match: ["*"],
27
+ exclude: [],
28
+ priority: 0
29
+ };
30
+
31
+ // Parse comma-separated or key-value options: cascade: true, match: ["..."], exclude: ["..."]
32
+ const pairs = rawDirective.split(/,(?![^[]*\])/);
33
+ for (const pair of pairs) {
34
+ const colonIdx = pair.indexOf(":");
35
+ if (colonIdx === -1) continue;
36
+
37
+ const key = pair.slice(0, colonIdx).trim().toLowerCase();
38
+ const valStr = pair.slice(colonIdx + 1).trim();
39
+
40
+ if (key === "cascade") {
41
+ pragma.cascade = valStr === "true" || valStr === "1";
42
+ } else if (key === "priority") {
43
+ const num = Number.parseInt(valStr, 10);
44
+ if (!Number.isNaN(num)) pragma.priority = num;
45
+ } else if (key === "match" || key === "exclude") {
46
+ let items: string[] = [];
47
+ if (valStr.startsWith("[") && valStr.endsWith("]")) {
48
+ try {
49
+ items = JSON.parse(valStr.replace(/'/g, '"'));
50
+ } catch {
51
+ items = valStr
52
+ .slice(1, -1)
53
+ .split(",")
54
+ .map((s) => s.trim().replace(/^['"]|['"]$/g, ""))
55
+ .filter(Boolean);
56
+ }
57
+ } else {
58
+ items = [valStr.replace(/^['"]|['"]$/g, "")];
59
+ }
60
+
61
+ if (key === "match") pragma.match = items;
62
+ if (key === "exclude") pragma.exclude = items;
63
+ }
64
+ }
65
+
66
+ return pragma;
67
+ }
68
+
69
+ /**
70
+ * Convert a glob-like pattern (e.g. `admin/*`, `users/**`, `stats`) into a RegExp
71
+ */
72
+ export function globToRegExp(pattern: string): RegExp {
73
+ const normalized = pattern.trim().replace(/^\/+|\/+$/g, "");
74
+ if (normalized === "*" || normalized === "**" || normalized === "") {
75
+ return /^.*$/;
76
+ }
77
+
78
+ const escaped = normalized
79
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
80
+ .replace(/\*\*/g, "___DOUBLE_STAR___")
81
+ .replace(/\*/g, "[^/]+")
82
+ .replace(/___DOUBLE_STAR___/g, ".*");
83
+
84
+ return new RegExp(`^(?:/)?${escaped}(?:/)?$`);
85
+ }
86
+
87
+ /**
88
+ * Test whether a normalized route path matches any glob pattern in a list
89
+ */
90
+ export function matchesAnyPattern(routePath: string, patterns: string[]): boolean {
91
+ if (!patterns || patterns.length === 0) return false;
92
+ const cleanPath = routePath.replace(/^\/+|\/+$/g, "");
93
+
94
+ for (const pat of patterns) {
95
+ const cleanPat = pat.replace(/^\/+|\/+$/g, "");
96
+ if (cleanPat === "*" || cleanPat === "**") return true;
97
+ if (cleanPat === cleanPath) return true;
98
+ if (globToRegExp(cleanPat).test(cleanPath)) return true;
99
+ }
100
+
101
+ return false;
102
+ }
103
+
104
+ /**
105
+ * Evaluate if a middleware definition should apply to a specific route
106
+ */
107
+ export function shouldApplyMiddleware(
108
+ pragma: MiddlewarePragma,
109
+ routePath: string,
110
+ routeDir: string,
111
+ middlewareDir: string
112
+ ): boolean {
113
+ const normRouteDir = routeDir.replace(/^\/+|\/+$/g, "");
114
+ const normMwDir = middlewareDir.replace(/^\/+|\/+$/g, "");
115
+
116
+ // 1. Ancestor check: routeDir must be equal to or a subfolder of middlewareDir
117
+ const isDirectDir = normRouteDir === normMwDir;
118
+ const isSubDir =
119
+ normMwDir === "" ? normRouteDir !== "" : normRouteDir.startsWith(`${normMwDir}/`);
120
+
121
+ if (!isDirectDir && !isSubDir) {
122
+ return false;
123
+ }
124
+
125
+ // 2. Cascading check: if route is in a subfolder and cascade is false, do not apply
126
+ const isCascade = pragma.cascade ?? true;
127
+ if (isSubDir && !isCascade) {
128
+ return false;
129
+ }
130
+
131
+ // 3. Relative route path within the middleware's domain
132
+ let relativePath = routePath.replace(/^\/+|\/+$/g, "");
133
+ if (normMwDir && relativePath.startsWith(normMwDir)) {
134
+ relativePath = relativePath.slice(normMwDir.length).replace(/^\/+/, "");
135
+ }
136
+
137
+ // 4. Exclude rule takes immediate precedence
138
+ if (pragma.exclude && pragma.exclude.length > 0) {
139
+ if (
140
+ matchesAnyPattern(routePath, pragma.exclude) ||
141
+ matchesAnyPattern(relativePath, pragma.exclude)
142
+ ) {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ // 5. Match rule
148
+ if (!pragma.match || pragma.match.length === 0 || pragma.match.includes("*")) {
149
+ return true;
150
+ }
151
+
152
+ return (
153
+ matchesAnyPattern(routePath, pragma.match) || matchesAnyPattern(relativePath, pragma.match)
154
+ );
155
+ }