@orieljs/routing 0.1.0-alpha.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/dist/index.js ADDED
@@ -0,0 +1,997 @@
1
+ // BaseRouter.ts
2
+ import {
3
+ CurrentRoute as CurrentRoute2,
4
+ UrlGenerator,
5
+ ViewFactory
6
+ } from "@orieljs/contracts";
7
+ import { currentContainer as currentContainer2 } from "@orieljs/container";
8
+
9
+ // dispatch.ts
10
+ import { CurrentRoute } from "@orieljs/contracts";
11
+ import { currentContainer } from "@orieljs/container";
12
+
13
+ // bindings.ts
14
+ import { RouteModelResolver } from "@orieljs/contracts";
15
+ import { BindingResolutionError } from "@orieljs/container";
16
+ import { Str } from "@orieljs/support";
17
+
18
+ // errors.ts
19
+ var NotFoundHttpError = class extends Error {
20
+ kind = "Http";
21
+ status = 404;
22
+ name = "NotFoundHttpError";
23
+ constructor(path) {
24
+ super(`The route ${path} could not be found.`);
25
+ }
26
+ };
27
+ var MethodNotAllowedHttpError = class extends Error {
28
+ kind = "Http";
29
+ status = 405;
30
+ headers;
31
+ name = "MethodNotAllowedHttpError";
32
+ constructor(method, path, allowed) {
33
+ super(`The ${method} method is not supported for route ${path}. Supported methods: ${allowed.join(", ")}.`);
34
+ this.headers = { Allow: allowed.join(", ") };
35
+ }
36
+ };
37
+ var RouteNotDefinedError = class extends Error {
38
+ constructor(routeName) {
39
+ super(`Route [${routeName}] not defined.`);
40
+ this.routeName = routeName;
41
+ }
42
+ routeName;
43
+ kind = "Server";
44
+ status = 500;
45
+ name = "RouteNotDefinedError";
46
+ };
47
+ var UrlGenerationError = class extends Error {
48
+ kind = "Server";
49
+ status = 500;
50
+ name = "UrlGenerationError";
51
+ constructor(routeName, uri, missing) {
52
+ super(`Missing required parameter for [Route: ${routeName}] [URI: ${uri}] [Missing parameter: ${missing.join(", ")}].`);
53
+ }
54
+ };
55
+ var RouteModelNotFoundError = class extends Error {
56
+ constructor(model, value) {
57
+ super(`No query results for model [${model}] ${String(value)}`);
58
+ this.model = model;
59
+ this.ids = [value];
60
+ }
61
+ model;
62
+ kind = "ModelNotFound";
63
+ status = 404;
64
+ ids;
65
+ name = "RouteModelNotFoundError";
66
+ };
67
+ var RoutingError = class extends Error {
68
+ kind = "Server";
69
+ status = 500;
70
+ name = "RoutingError";
71
+ };
72
+
73
+ // middleware.ts
74
+ import {
75
+ ExceptionHandler,
76
+ PipelineFactory
77
+ } from "@orieljs/contracts";
78
+
79
+ // toResponse.ts
80
+ async function toResponse(request, value) {
81
+ if (value instanceof Response) return value;
82
+ if (value === null || value === void 0) return new Response(null, { status: 204 });
83
+ if (typeof value === "string") return html(value);
84
+ if (typeof value !== "object") return html(String(value));
85
+ const candidate = value;
86
+ if (typeof candidate.toResponse === "function") {
87
+ return toResponse(request, await candidate.toResponse.call(value, request));
88
+ }
89
+ if (typeof candidate.render === "function") return html(String(await candidate.render.call(value)));
90
+ if (typeof candidate.toHtml === "function") return html(String(candidate.toHtml.call(value)));
91
+ if (typeof candidate.toJSON !== "function") {
92
+ if (typeof candidate.toJson === "function") return json(String(candidate.toJson.call(value)));
93
+ if (typeof candidate.toArray === "function") return json(JSON.stringify(candidate.toArray.call(value)));
94
+ }
95
+ return json(JSON.stringify(value));
96
+ }
97
+ function html(body) {
98
+ return new Response(body, { headers: { "content-type": "text/html; charset=utf-8" } });
99
+ }
100
+ function json(body) {
101
+ return new Response(body, { headers: { "content-type": "application/json" } });
102
+ }
103
+
104
+ // middleware.ts
105
+ var MiddlewareNotFoundError = class extends Error {
106
+ constructor(middleware) {
107
+ super(
108
+ `Middleware [${middleware}] is not defined. Register it with Route.aliasMiddleware() or withMiddleware((m) => m.alias({ ... })).`
109
+ );
110
+ this.middleware = middleware;
111
+ }
112
+ middleware;
113
+ kind = "Server";
114
+ status = 500;
115
+ name = "MiddlewareNotFoundError";
116
+ };
117
+ function expand(references, aliases, groups, visiting, out) {
118
+ for (const reference of references) {
119
+ if (typeof reference === "function") {
120
+ out.push({ cls: reference, parameters: [] });
121
+ continue;
122
+ }
123
+ const index = reference.indexOf(":");
124
+ const name = index === -1 ? reference : reference.slice(0, index);
125
+ const parameters = index === -1 ? [] : reference.slice(index + 1).split(",");
126
+ if (index === -1 && Object.hasOwn(groups, name)) {
127
+ if (visiting.has(name)) throw new RoutingError(`Middleware group [${name}] includes itself.`);
128
+ visiting.add(name);
129
+ expand(groups[name], aliases, groups, visiting, out);
130
+ visiting.delete(name);
131
+ continue;
132
+ }
133
+ if (!Object.hasOwn(aliases, name)) throw new MiddlewareNotFoundError(reference);
134
+ out.push({ cls: aliases[name], parameters });
135
+ }
136
+ }
137
+ function resolveMiddleware(references, excluded, aliases, groups) {
138
+ const all = [];
139
+ expand(references, aliases, groups, /* @__PURE__ */ new Set(), all);
140
+ const skipped = [];
141
+ expand(excluded, aliases, groups, /* @__PURE__ */ new Set(), skipped);
142
+ const skippedClasses = new Set(skipped.map((entry) => entry.cls));
143
+ const seen = /* @__PURE__ */ new Set();
144
+ const classIds = /* @__PURE__ */ new Map();
145
+ return all.filter((entry) => {
146
+ if (skippedClasses.has(entry.cls)) return false;
147
+ if (!classIds.has(entry.cls)) classIds.set(entry.cls, classIds.size);
148
+ const key = `${classIds.get(entry.cls)}:${entry.parameters.join(",")}`;
149
+ if (seen.has(key)) return false;
150
+ seen.add(key);
151
+ return true;
152
+ });
153
+ }
154
+ async function renderError(container, request, error) {
155
+ if (!container.bound(ExceptionHandler)) throw error;
156
+ const handler = container.make(ExceptionHandler);
157
+ await handler.report(error);
158
+ return handler.render(request, error);
159
+ }
160
+ async function guard(container, request, run) {
161
+ try {
162
+ const result = await run();
163
+ return result instanceof Response ? result : await toResponse(request, result);
164
+ } catch (error) {
165
+ return renderError(container, request, error);
166
+ }
167
+ }
168
+ async function runMiddleware(container, request, middleware, instances, destination, stage) {
169
+ const pipes = middleware.map(({ cls, parameters }) => {
170
+ const instance = container.make(cls);
171
+ instances.push(instance);
172
+ return (passable, next) => guard(container, passable, () => instance.handle(passable, next, ...parameters));
173
+ });
174
+ if (stage !== void 0) {
175
+ pipes.splice(
176
+ stage.at,
177
+ 0,
178
+ (passable, next) => guard(container, passable, async () => {
179
+ await stage.run(passable);
180
+ return next(passable);
181
+ })
182
+ );
183
+ }
184
+ return container.make(PipelineFactory).make().send(request).through(pipes).then((passable) => guard(container, passable, () => destination(passable)));
185
+ }
186
+
187
+ // bindings.ts
188
+ function orderForBindings(router, middleware) {
189
+ const aliases = router.getMiddleware();
190
+ const groups = router.getMiddlewareGroups();
191
+ const early = /* @__PURE__ */ new Set();
192
+ for (const name of ["auth", "guest"]) if (Object.hasOwn(aliases, name)) early.add(aliases[name]);
193
+ for (const group of ["web", "api"]) {
194
+ try {
195
+ for (const entry of resolveMiddleware([group], [], aliases, groups)) early.add(entry.cls);
196
+ } catch {
197
+ }
198
+ }
199
+ const before = middleware.filter((entry) => early.has(entry.cls));
200
+ const after = middleware.filter((entry) => !early.has(entry.cls));
201
+ return { middleware: [...before, ...after], at: before.length };
202
+ }
203
+ async function substituteBindings(router, route, values, container) {
204
+ const resolver = container.bound(RouteModelResolver) ? container.make(RouteModelResolver) : void 0;
205
+ const bound = {};
206
+ for (const { name, field } of route.parameters) {
207
+ const value = values[name];
208
+ if (value === void 0) {
209
+ bound[name] = void 0;
210
+ continue;
211
+ }
212
+ const binder = router.getBinder(name);
213
+ if (binder !== void 0) {
214
+ bound[name] = await binder(value);
215
+ continue;
216
+ }
217
+ const model = router.getModel(name);
218
+ if (model !== void 0) {
219
+ if (resolver === void 0) {
220
+ throw new BindingResolutionError(
221
+ `Route.model('${name}', ${model.name}) needs a RouteModelResolver. Is the Database service provider registered?`
222
+ );
223
+ }
224
+ const found = await resolver.resolveExplicit(model, value, field);
225
+ if (found === null || found === void 0) throw new RouteModelNotFoundError(model.name, value);
226
+ bound[name] = found;
227
+ continue;
228
+ }
229
+ if (resolver !== void 0) {
230
+ const found = await resolver.resolve(name, value, field);
231
+ if (found === null) throw new RouteModelNotFoundError(Str.studly(name), value);
232
+ bound[name] = found === void 0 ? value : found;
233
+ continue;
234
+ }
235
+ bound[name] = value;
236
+ }
237
+ return bound;
238
+ }
239
+
240
+ // RouteDefinition.ts
241
+ var PARAMETER = /\{(\w+)(?::(\w+))?(\?)?\}/g;
242
+ var WHOLE_OPTIONAL = /^\{(\w+)(?::\w+)?\?\}$/;
243
+ var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
244
+ function parseParameters(uri) {
245
+ return [...uri.matchAll(PARAMETER)].map((match) => ({ name: match[1], optional: match[3] === "?", field: match[2] }));
246
+ }
247
+ function normalizeUri(uri) {
248
+ const trimmed = uri.replace(/^\/+|\/+$/g, "");
249
+ return trimmed === "" ? "/" : trimmed;
250
+ }
251
+ function normalizePath(pathname) {
252
+ const trimmed = pathname.replace(/^\/+|\/+$/g, "").replace(/\/{2,}/g, "/");
253
+ return trimmed === "" ? "" : `/${trimmed}`;
254
+ }
255
+ function decode(value) {
256
+ try {
257
+ return decodeURIComponent(value);
258
+ } catch {
259
+ return value;
260
+ }
261
+ }
262
+ function compileBody(uri, wheres) {
263
+ if (uri === "/") return "";
264
+ const group = (name) => `(?<p_${name}>${wheres[name] ?? "[^/]+"})`;
265
+ let body = "";
266
+ for (const segment of uri.split("/")) {
267
+ const whole = WHOLE_OPTIONAL.exec(segment);
268
+ if (whole) {
269
+ body += `(?:/${group(whole[1])})?`;
270
+ continue;
271
+ }
272
+ let piece = "";
273
+ let last = 0;
274
+ for (const match of segment.matchAll(PARAMETER)) {
275
+ piece += escapeRegex(segment.slice(last, match.index)) + group(match[1]) + (match[3] === "?" ? "?" : "");
276
+ last = match.index + match[0].length;
277
+ }
278
+ body += `/${piece}${escapeRegex(segment.slice(last))}`;
279
+ }
280
+ return body;
281
+ }
282
+ var FALLBACK_SEGMENT = "{fallbackPlaceholder}";
283
+ function compileFallback(uri, wheres) {
284
+ const tail = "(?:/(?<p_fallbackPlaceholder>.*))?";
285
+ if (uri === FALLBACK_SEGMENT) return new RegExp(`^${tail}$`, "s");
286
+ if (!uri.endsWith(`/${FALLBACK_SEGMENT}`)) return new RegExp(`^${compileBody(uri, { ...wheres, fallbackPlaceholder: ".*" })}$`, "s");
287
+ return new RegExp(`^${compileBody(uri.slice(0, -FALLBACK_SEGMENT.length - 1), wheres)}${tail}$`, "s");
288
+ }
289
+ function compareRoutes(a, b) {
290
+ if (a.isFallback !== b.isFallback) return a.isFallback ? 1 : -1;
291
+ const left = a.rank();
292
+ const right = b.rank();
293
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
294
+ const x = left[i] ?? -1;
295
+ const y = right[i] ?? -1;
296
+ if (x !== y) return x - y;
297
+ }
298
+ return 0;
299
+ }
300
+ var RouteDefinition = class {
301
+ methods;
302
+ uri;
303
+ action;
304
+ controller;
305
+ isFallback;
306
+ parameters;
307
+ #name;
308
+ #middleware;
309
+ #excluded = [];
310
+ #wheres = {};
311
+ #compiled;
312
+ constructor(methods, uri, action, attributes = {}) {
313
+ this.methods = [...new Set(methods.map((method) => method.toUpperCase()))];
314
+ this.uri = normalizeUri(uri);
315
+ this.action = action;
316
+ this.controller = attributes.controller;
317
+ this.isFallback = attributes.fallback ?? false;
318
+ this.parameters = parseParameters(this.uri);
319
+ this.#name = attributes.namePrefix === void 0 || attributes.namePrefix === "" ? void 0 : attributes.namePrefix;
320
+ this.#middleware = [...attributes.middleware ?? []];
321
+ }
322
+ getName() {
323
+ return this.#name;
324
+ }
325
+ /** Appends, like Laravel's $action['as'] .= $name. */
326
+ name(name) {
327
+ this.#name = (this.#name ?? "") + name;
328
+ return this;
329
+ }
330
+ middleware(...middleware) {
331
+ this.#middleware.push(...middleware.flat());
332
+ return this;
333
+ }
334
+ withoutMiddleware(...middleware) {
335
+ this.#excluded.push(...middleware.flat());
336
+ return this;
337
+ }
338
+ where(parameter, pattern) {
339
+ if (typeof parameter === "string") this.#wheres[parameter] = pattern ?? "[^/]+";
340
+ else Object.assign(this.#wheres, parameter);
341
+ this.#compiled = void 0;
342
+ return this;
343
+ }
344
+ gatherMiddleware() {
345
+ return [...this.#middleware];
346
+ }
347
+ excludedMiddleware() {
348
+ return [...this.#excluded];
349
+ }
350
+ wheres() {
351
+ return { ...this.#wheres };
352
+ }
353
+ getActionName() {
354
+ if (typeof this.action === "string") return `${this.controller?.name ?? "Controller"}@${this.action}`;
355
+ if (Array.isArray(this.action)) return `${this.action[0].name}@${String(this.action[1])}`;
356
+ return "Closure";
357
+ }
358
+ /** Decoded parameter values when `path` (normalizePath output) matches, else undefined. */
359
+ match(path, patterns = {}, version = 0) {
360
+ const found = this.#regex(patterns, version).exec(path);
361
+ if (found === null) return void 0;
362
+ const values = {};
363
+ for (const { name } of this.parameters) {
364
+ const raw = found.groups?.[`p_${name}`];
365
+ values[name] = raw === void 0 ? void 0 : decode(raw);
366
+ }
367
+ return values;
368
+ }
369
+ /** Per-segment specificity: static 0, parameter 1, optional parameter 2. Lower sorts first. */
370
+ rank() {
371
+ if (this.uri === "/") return [];
372
+ return this.uri.split("/").map((segment) => WHOLE_OPTIONAL.test(segment) ? 2 : segment.includes("{") ? 1 : 0);
373
+ }
374
+ #regex(patterns, version) {
375
+ if (this.#compiled !== void 0 && this.#compiled.version === version) return this.#compiled.regex;
376
+ const wheres = { ...patterns, ...this.#wheres };
377
+ const regex = this.isFallback ? compileFallback(this.uri, wheres) : new RegExp(`^${compileBody(this.uri, wheres)}$`);
378
+ this.#compiled = { regex, version };
379
+ return regex;
380
+ }
381
+ };
382
+
383
+ // dispatch.ts
384
+ var ControllerMethodNotFoundError = class extends Error {
385
+ kind = "Server";
386
+ status = 500;
387
+ name = "ControllerMethodNotFoundError";
388
+ constructor(controller, method) {
389
+ super(`Method ${controller}::${method} does not exist.`);
390
+ }
391
+ };
392
+ var isClass = (value) => typeof value === "function" && /^class[\s{]/.test(Function.prototype.toString.call(value));
393
+ function findRoute(router, request) {
394
+ const method = request.method().toUpperCase();
395
+ const path = normalizePath(new URL(request.raw.url).pathname);
396
+ const patterns = router.getPatterns();
397
+ const version = router.getPatternVersion();
398
+ const allowed = /* @__PURE__ */ new Set();
399
+ for (const route of router.getRoutesByMatchOrder()) {
400
+ const values = route.match(path, patterns, version);
401
+ if (values === void 0) continue;
402
+ if (route.methods.includes(method)) return { route, values };
403
+ if (!route.isFallback) for (const verb of route.methods) allowed.add(verb);
404
+ }
405
+ if (allowed.size > 0) {
406
+ if (method === "OPTIONS") return { response: new Response(null, { status: 200, headers: { Allow: [...allowed].join(", ") } }) };
407
+ throw new MethodNotAllowedHttpError(method, request.path(), [...allowed]);
408
+ }
409
+ throw new NotFoundHttpError(request.path());
410
+ }
411
+ async function runAction(scope, route, parameters) {
412
+ const args = route.parameters.map((parameter) => parameters[parameter.name]);
413
+ const action = route.action;
414
+ if (typeof action === "function" && !isClass(action)) return action(...args);
415
+ const [cls, method] = typeof action === "string" ? [route.controller, action] : isClass(action) ? [action, "__invoke"] : [action[0], String(action[1])];
416
+ const controller = scope.make(cls);
417
+ const handler = controller[method];
418
+ if (typeof handler !== "function") throw new ControllerMethodNotFoundError(cls.name, method);
419
+ return handler.apply(controller, args);
420
+ }
421
+ async function dispatchRoute(router, request, container) {
422
+ const scope = currentContainer() ?? container;
423
+ if (scope === void 0) throw new RoutingError("Cannot dispatch a request: no container is available.");
424
+ const match = findRoute(router, request);
425
+ if ("response" in match) return match.response;
426
+ const { route, values } = match;
427
+ const state = { route, parameters: { ...values }, middleware: [] };
428
+ scope.instance(CurrentRoute, state);
429
+ const middleware = resolveMiddleware(
430
+ route.gatherMiddleware(),
431
+ route.excludedMiddleware(),
432
+ router.getMiddleware(),
433
+ router.getMiddlewareGroups()
434
+ );
435
+ const ordered = orderForBindings(router, middleware);
436
+ return runMiddleware(scope, request, ordered.middleware, state.middleware, () => runAction(scope, route, state.parameters), {
437
+ at: ordered.at,
438
+ run: async () => void Object.assign(state.parameters, await substituteBindings(router, route, values, scope))
439
+ });
440
+ }
441
+
442
+ // GroupRegistrar.ts
443
+ var ALL_VERBS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
444
+ function mergeGroup(parent, child) {
445
+ const prefix = [parent.prefix, child.prefix].map((part) => (part ?? "").replace(/^\/+|\/+$/g, "")).filter((part) => part !== "").join("/");
446
+ const merged = {
447
+ middleware: [...parent.middleware ?? [], ...child.middleware ?? []]
448
+ };
449
+ if (prefix !== "") merged.prefix = prefix;
450
+ const as = (parent.as ?? "") + (child.as ?? "");
451
+ if (as !== "") merged.as = as;
452
+ const controller = child.controller ?? parent.controller;
453
+ if (controller !== void 0) merged.controller = controller;
454
+ return merged;
455
+ }
456
+ var GroupRegistrar = class {
457
+ constructor(router) {
458
+ this.router = router;
459
+ }
460
+ router;
461
+ #attributes = {};
462
+ prefix(prefix) {
463
+ this.#attributes.prefix = prefix;
464
+ return this;
465
+ }
466
+ name(prefix) {
467
+ this.#attributes.as = prefix;
468
+ return this;
469
+ }
470
+ middleware(...middleware) {
471
+ this.#attributes.middleware = [...this.#attributes.middleware ?? [], ...middleware.flat()];
472
+ return this;
473
+ }
474
+ controller(controller) {
475
+ this.#attributes.controller = controller;
476
+ return this;
477
+ }
478
+ group(routes) {
479
+ return this.router.group({ ...this.#attributes }, routes);
480
+ }
481
+ get(uri, action) {
482
+ return this.#one(["GET", "HEAD"], uri, action);
483
+ }
484
+ post(uri, action) {
485
+ return this.#one(["POST"], uri, action);
486
+ }
487
+ put(uri, action) {
488
+ return this.#one(["PUT"], uri, action);
489
+ }
490
+ patch(uri, action) {
491
+ return this.#one(["PATCH"], uri, action);
492
+ }
493
+ delete(uri, action) {
494
+ return this.#one(["DELETE"], uri, action);
495
+ }
496
+ options(uri, action) {
497
+ return this.#one(["OPTIONS"], uri, action);
498
+ }
499
+ any(uri, action) {
500
+ return this.#one(ALL_VERBS, uri, action);
501
+ }
502
+ match(methods, uri, action) {
503
+ return this.#one(methods, uri, action);
504
+ }
505
+ resource(name, controller, options) {
506
+ void this.router.group({ ...this.#attributes }, () => this.router.resource(name, controller, options));
507
+ }
508
+ apiResource(name, controller, options) {
509
+ void this.router.group({ ...this.#attributes }, () => this.router.apiResource(name, controller, options));
510
+ }
511
+ #one(methods, uri, action) {
512
+ let route;
513
+ void this.router.group({ ...this.#attributes }, () => {
514
+ route = this.router.addRoute(methods, uri, action);
515
+ });
516
+ return route;
517
+ }
518
+ };
519
+
520
+ // resource.ts
521
+ import { Str as Str2 } from "@orieljs/support";
522
+ var DEFAULTS = ["index", "create", "store", "show", "edit", "update", "destroy"];
523
+ var API_DEFAULTS = ["index", "store", "show", "update", "destroy"];
524
+ function wildcard(segment, options) {
525
+ const renamed = options.parameters?.[segment];
526
+ return (renamed ?? Str2.singular(segment)).replace(/-/g, "_");
527
+ }
528
+ function resourceRoutes(name, options = {}, api = false) {
529
+ const segments = name.split(".");
530
+ const last = segments.at(-1);
531
+ const base = [...segments.slice(0, -1).map((segment) => `${segment}/{${wildcard(segment, options)}}`), last].join("/");
532
+ const member = `${base}/{${wildcard(last, options)}}`;
533
+ let methods = api ? API_DEFAULTS : DEFAULTS;
534
+ if (options.only !== void 0) methods = methods.filter((method) => options.only.includes(method));
535
+ if (options.except !== void 0) methods = methods.filter((method) => !options.except.includes(method));
536
+ const table = {
537
+ index: { verbs: ["GET", "HEAD"], uri: base },
538
+ create: { verbs: ["GET", "HEAD"], uri: `${base}/create` },
539
+ store: { verbs: ["POST"], uri: base },
540
+ show: { verbs: ["GET", "HEAD"], uri: member },
541
+ edit: { verbs: ["GET", "HEAD"], uri: `${member}/edit` },
542
+ update: { verbs: ["PUT", "PATCH"], uri: member },
543
+ destroy: { verbs: ["DELETE"], uri: member }
544
+ };
545
+ return methods.map((method) => ({
546
+ method,
547
+ verbs: table[method].verbs,
548
+ uri: table[method].uri,
549
+ name: options.names?.[method] ?? `${name}.${method}`
550
+ }));
551
+ }
552
+
553
+ // BaseRouter.ts
554
+ var BaseRouter = class {
555
+ /** `container` is the application; request-time lookups use the current (request) container first. */
556
+ constructor(container) {
557
+ this.container = container;
558
+ }
559
+ container;
560
+ #routes = [];
561
+ #sorted;
562
+ #groupStack = [];
563
+ #patterns = {};
564
+ #patternVersion = 0;
565
+ #aliases = {};
566
+ #groups = { web: [], api: [] };
567
+ #binders = /* @__PURE__ */ new Map();
568
+ #models = /* @__PURE__ */ new Map();
569
+ get(uri, action) {
570
+ return this.addRoute(["GET", "HEAD"], uri, action);
571
+ }
572
+ post(uri, action) {
573
+ return this.addRoute(["POST"], uri, action);
574
+ }
575
+ put(uri, action) {
576
+ return this.addRoute(["PUT"], uri, action);
577
+ }
578
+ patch(uri, action) {
579
+ return this.addRoute(["PATCH"], uri, action);
580
+ }
581
+ delete(uri, action) {
582
+ return this.addRoute(["DELETE"], uri, action);
583
+ }
584
+ options(uri, action) {
585
+ return this.addRoute(["OPTIONS"], uri, action);
586
+ }
587
+ any(uri, action) {
588
+ return this.addRoute(ALL_VERBS, uri, action);
589
+ }
590
+ match(methods, uri, action) {
591
+ return this.addRoute(methods, uri, action);
592
+ }
593
+ /** Registers a route with the current group's prefix, name prefix, middleware and controller. GET implies HEAD. */
594
+ addRoute(methods, uri, action, options = {}) {
595
+ const group = this.#groupStack.at(-1) ?? {};
596
+ const verbs = methods.map((method) => method.toUpperCase());
597
+ if (verbs.includes("GET") && !verbs.includes("HEAD")) verbs.push("HEAD");
598
+ if (typeof action === "string" && group.controller === void 0) {
599
+ throw new RoutingError(
600
+ `Route action [${action}] needs a controller. Use Route.controller(X).group(...) or pass [Controller, '${action}'].`
601
+ );
602
+ }
603
+ const prefix = (group.prefix ?? "").replace(/^\/+|\/+$/g, "");
604
+ const path = uri.replace(/^\/+|\/+$/g, "");
605
+ const route = new RouteDefinition(verbs, [prefix, path].filter((part) => part !== "").join("/"), action, {
606
+ ...group.as !== void 0 && { namePrefix: group.as },
607
+ ...group.middleware !== void 0 && { middleware: group.middleware },
608
+ ...group.controller !== void 0 && { controller: group.controller },
609
+ ...options.fallback === true && { fallback: true }
610
+ });
611
+ this.#routes.push(route);
612
+ this.#sorted = void 0;
613
+ return route;
614
+ }
615
+ /** Route.redirect('/here', '/there'): any verb, 302 by default; '{param}' in the target is filled from the URI. */
616
+ redirect(from, to, status = 302) {
617
+ const holder = {};
618
+ const action = (...values) => {
619
+ const parameters = new Map(holder.route.parameters.map((parameter, index) => [parameter.name, values[index]]));
620
+ const target = to.replace(/\{(\w+)\??\}/g, (_match, name) => {
621
+ const value = parameters.get(name);
622
+ return value === void 0 || value === null ? "" : encodeURIComponent(String(value));
623
+ });
624
+ const container = currentContainer2() ?? this.container;
625
+ const location = container?.bound(UrlGenerator) ? container.make(UrlGenerator).to(target) : target;
626
+ return new Response(null, { status, headers: { Location: location } });
627
+ };
628
+ holder.route = this.any(from, action);
629
+ return holder.route;
630
+ }
631
+ permanentRedirect(from, to) {
632
+ return this.redirect(from, to, 301);
633
+ }
634
+ /** Route.view('/about', 'pages.about', data): GET/HEAD; route parameters are merged into the view data. */
635
+ view(uri, view, data = {}) {
636
+ const holder = {};
637
+ const action = (...values) => {
638
+ const parameters = Object.fromEntries(holder.route.parameters.map((parameter, index) => [parameter.name, values[index]]));
639
+ const container = currentContainer2() ?? this.container;
640
+ if (container === void 0) throw new RoutingError(`Cannot render view [${view}]: no container is available.`);
641
+ return container.make(ViewFactory).make(view, { ...data, ...parameters });
642
+ };
643
+ holder.route = this.addRoute(["GET", "HEAD"], uri, action);
644
+ return holder.route;
645
+ }
646
+ resource(name, controller, options = {}) {
647
+ this.#registerResource(name, controller, options, false);
648
+ }
649
+ apiResource(name, controller, options = {}) {
650
+ this.#registerResource(name, controller, options, true);
651
+ }
652
+ /** Matches any GET path no other route matches. Always tried last. */
653
+ fallback(action) {
654
+ return this.addRoute(["GET", "HEAD"], "{fallbackPlaceholder}", action, { fallback: true });
655
+ }
656
+ prefix(prefix) {
657
+ return new GroupRegistrar(this).prefix(prefix);
658
+ }
659
+ name(prefix) {
660
+ return new GroupRegistrar(this).name(prefix);
661
+ }
662
+ middleware(...middleware) {
663
+ return new GroupRegistrar(this).middleware(...middleware);
664
+ }
665
+ controller(controller) {
666
+ return new GroupRegistrar(this).controller(controller);
667
+ }
668
+ /**
669
+ * Laravel's Route::group(attributes, callback). A synchronous callback's attributes are popped before this
670
+ * returns, so an unawaited group never leaks into later routes; an async callback keeps them until it settles.
671
+ */
672
+ group(attributes, routes) {
673
+ const entry = mergeGroup(this.#groupStack.at(-1) ?? {}, attributes);
674
+ this.#groupStack.push(entry);
675
+ const pop = () => {
676
+ const index = this.#groupStack.lastIndexOf(entry);
677
+ if (index !== -1) this.#groupStack.splice(index, 1);
678
+ };
679
+ let result;
680
+ try {
681
+ result = routes();
682
+ } catch (error) {
683
+ pop();
684
+ throw error;
685
+ }
686
+ if (result instanceof Promise) return result.finally(pop);
687
+ pop();
688
+ return Promise.resolve();
689
+ }
690
+ model(parameter, model) {
691
+ this.#models.set(parameter, model);
692
+ }
693
+ bind(parameter, resolver) {
694
+ this.#binders.set(parameter, resolver);
695
+ }
696
+ pattern(parameter, pattern) {
697
+ this.#patterns[parameter] = pattern;
698
+ this.#patternVersion++;
699
+ }
700
+ patterns(patterns) {
701
+ for (const [parameter, pattern] of Object.entries(patterns)) this.pattern(parameter, pattern);
702
+ }
703
+ getPatterns() {
704
+ return { ...this.#patterns };
705
+ }
706
+ /** Bumped by pattern(); routes recompile their matcher when it changes. */
707
+ getPatternVersion() {
708
+ return this.#patternVersion;
709
+ }
710
+ getBinder(parameter) {
711
+ return this.#binders.get(parameter);
712
+ }
713
+ getModel(parameter) {
714
+ return this.#models.get(parameter);
715
+ }
716
+ aliasMiddleware(name, middleware) {
717
+ this.#aliases[name] = middleware;
718
+ return this;
719
+ }
720
+ /** The middleware aliases (Laravel's Router::getMiddleware()). */
721
+ getMiddleware() {
722
+ return { ...this.#aliases };
723
+ }
724
+ middlewareGroup(name, middleware) {
725
+ this.#groups[name] = [...middleware];
726
+ return this;
727
+ }
728
+ pushMiddlewareToGroup(group, middleware) {
729
+ const list = this.#groups[group] ??= [];
730
+ if (!list.includes(middleware)) list.push(middleware);
731
+ return this;
732
+ }
733
+ prependMiddlewareToGroup(group, middleware) {
734
+ const list = this.#groups[group] ??= [];
735
+ if (!list.includes(middleware)) list.unshift(middleware);
736
+ return this;
737
+ }
738
+ hasMiddlewareGroup(name) {
739
+ return Object.hasOwn(this.#groups, name);
740
+ }
741
+ getMiddlewareGroups() {
742
+ return Object.fromEntries(Object.entries(this.#groups).map(([name, list]) => [name, [...list]]));
743
+ }
744
+ /** Every route, in registration order. */
745
+ getRoutes() {
746
+ return [...this.#routes];
747
+ }
748
+ /** Routes in matching order: static segments first, fallbacks last (cached until a route is added). */
749
+ getRoutesByMatchOrder() {
750
+ return this.#sorted ??= [...this.#routes].sort(compareRoutes);
751
+ }
752
+ /** The last route registered with this name wins, as in Laravel. */
753
+ getByName(name) {
754
+ return this.#routes.findLast((route) => route.getName() === name);
755
+ }
756
+ has(...names) {
757
+ return names.every((name) => this.getByName(name) !== void 0);
758
+ }
759
+ /** The route matched for the current request (read from the request scope). */
760
+ current() {
761
+ const container = currentContainer2();
762
+ if (container === void 0 || !container.bound(CurrentRoute2)) return void 0;
763
+ return container.make(CurrentRoute2).route;
764
+ }
765
+ currentRouteName() {
766
+ return this.current()?.getName();
767
+ }
768
+ /** Match, bind and run the route middleware and action (Router.dispatch; called by the Http kernel). */
769
+ dispatch(request) {
770
+ return dispatchRoute(this, request, this.container);
771
+ }
772
+ #registerResource(name, controller, options, api) {
773
+ for (const entry of resourceRoutes(name, options, api)) {
774
+ this.addRoute(entry.verbs, entry.uri, [controller, entry.method]).name(entry.name);
775
+ }
776
+ }
777
+ };
778
+
779
+ // BaseUrlGenerator.ts
780
+ import { ConfigRepository, HttpRequest, Session } from "@orieljs/contracts";
781
+ import { currentContainer as currentContainer3 } from "@orieljs/container";
782
+ var VALID_URL = /^(#|\/\/|https?:\/\/|(mailto|tel|sms):)/i;
783
+ var isPlainObject = (value) => {
784
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
785
+ const prototype = Object.getPrototypeOf(value);
786
+ return prototype === Object.prototype || prototype === null;
787
+ };
788
+ function buildQuery(values) {
789
+ const pairs = [];
790
+ const add = (key, value) => {
791
+ if (value === null || value === void 0) return;
792
+ if (typeof value === "object" && (Array.isArray(value) || isPlainObject(value))) {
793
+ for (const [child, nested] of Object.entries(value)) add(`${key}[${child}]`, nested);
794
+ return;
795
+ }
796
+ const text = value === true ? "1" : value === false ? "0" : String(value);
797
+ pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(text)}`);
798
+ };
799
+ for (const [key, value] of Object.entries(values)) add(key, value);
800
+ return pairs.join("&");
801
+ }
802
+ function routeKey(value, field) {
803
+ if (typeof value !== "object" || value === null) return value;
804
+ const model = value;
805
+ if (field !== void 0) return typeof model.getAttribute === "function" ? model.getAttribute(field) : model[field];
806
+ return typeof model.getRouteKey === "function" ? model.getRouteKey() : value;
807
+ }
808
+ var BaseUrlGenerator = class {
809
+ constructor(router, container) {
810
+ this.router = router;
811
+ this.container = container;
812
+ }
813
+ router;
814
+ container;
815
+ /** 'http://localhost' (no trailing slash). */
816
+ root() {
817
+ const request = this.#request();
818
+ if (request !== void 0) return new URL(request.url()).origin;
819
+ const configured = this.#config("app.url", void 0);
820
+ return (configured ?? "http://localhost").replace(/\/+$/, "");
821
+ }
822
+ to(path, query = {}, absolute = true) {
823
+ const queryString = buildQuery(query);
824
+ const base = VALID_URL.test(path) ? path : this.#join(path, absolute);
825
+ if (queryString === "") return base;
826
+ return `${base}${base.includes("?") ? "&" : "?"}${queryString}`;
827
+ }
828
+ /**
829
+ * route('posts.show', post) / route('posts.show', [post, comment]) / route('posts.show', { post: 1, page: 2 }).
830
+ * Named values fill their parameters, positional values fill the rest in URI order; extra named values become
831
+ * the query string, extra positional ones are dropped.
832
+ */
833
+ route(name, parameters, absolute = true) {
834
+ const route = this.router.getByName(name);
835
+ if (route === void 0) throw new RouteNotDefinedError(name);
836
+ const named = isPlainObject(parameters) ? { ...parameters } : {};
837
+ const positional = Array.isArray(parameters) ? [...parameters] : parameters === void 0 || parameters === null || isPlainObject(parameters) ? [] : [parameters];
838
+ const values = /* @__PURE__ */ new Map();
839
+ const missing = [];
840
+ for (const parameter of route.parameters) {
841
+ let value;
842
+ if (Object.hasOwn(named, parameter.name)) {
843
+ value = named[parameter.name];
844
+ delete named[parameter.name];
845
+ } else if (positional.length > 0) {
846
+ value = positional.shift();
847
+ }
848
+ const key = routeKey(value, parameter.field);
849
+ const text = key === void 0 || key === null ? void 0 : String(key);
850
+ if (text === void 0 && !parameter.optional) missing.push(parameter.name);
851
+ values.set(parameter.name, text);
852
+ }
853
+ if (missing.length > 0) throw new UrlGenerationError(name, route.uri, missing);
854
+ const path = route.uri === "/" ? "" : route.uri.split("/").filter((segment) => {
855
+ const whole = WHOLE_OPTIONAL.exec(segment);
856
+ return whole === null || values.get(whole[1]) !== void 0;
857
+ }).map(
858
+ (segment) => segment.replace(PARAMETER, (_match, parameter) => {
859
+ const value = values.get(parameter);
860
+ return value === void 0 ? "" : encodeURIComponent(value);
861
+ })
862
+ ).join("/");
863
+ const query = Object.fromEntries(Object.entries(named).map(([key, value]) => [key, routeKey(value, void 0)]));
864
+ return this.to(path, query, absolute);
865
+ }
866
+ asset(path) {
867
+ if (VALID_URL.test(path)) return path;
868
+ const base = this.#config("app.asset_url", void 0)?.replace(/\/+$/, "") ?? this.root();
869
+ const trimmed = path.replace(/^\/+/, "");
870
+ return trimmed === "" ? base : `${base}/${trimmed}`;
871
+ }
872
+ /** The current URL without the query string. */
873
+ current() {
874
+ return this.#request()?.url() ?? this.root();
875
+ }
876
+ /** The current URL with the query string. */
877
+ full() {
878
+ return this.#request()?.fullUrl() ?? this.root();
879
+ }
880
+ /** The Referer header, then the session's previous URL, then `fallback`, then the root (Laravel's order). */
881
+ previous(fallback) {
882
+ const scope = this.#scope();
883
+ const request = this.#request();
884
+ const referer = request?.header("referer");
885
+ if (referer !== void 0 && referer !== "") return this.to(referer);
886
+ if (request !== void 0 && scope?.bound(Session)) {
887
+ const previous = scope.make(Session).previousUrl();
888
+ if (previous !== void 0 && previous !== "") return this.to(previous);
889
+ }
890
+ return this.to(fallback ?? "/");
891
+ }
892
+ hasRoute(name) {
893
+ return this.router.getByName(name) !== void 0;
894
+ }
895
+ #join(path, absolute) {
896
+ const trimmed = path.replace(/^\/+|\/+$/g, "");
897
+ if (!absolute) return `/${trimmed}`;
898
+ return trimmed === "" ? this.root() : `${this.root()}/${trimmed}`;
899
+ }
900
+ #scope() {
901
+ return currentContainer3() ?? this.container;
902
+ }
903
+ /** The HttpRequest, only when running inside a request scope. */
904
+ #request() {
905
+ const scope = this.#scope();
906
+ return scope?.bound(HttpRequest) ? scope.make(HttpRequest) : void 0;
907
+ }
908
+ #config(key, fallback) {
909
+ const scope = this.#scope();
910
+ return scope?.bound(ConfigRepository) ? scope.make(ConfigRepository).get(key, fallback) : fallback;
911
+ }
912
+ };
913
+
914
+ // facade.ts
915
+ import { Router } from "@orieljs/contracts";
916
+ import { facade } from "@orieljs/support";
917
+ var Route = facade(Router);
918
+
919
+ // routeFiles.ts
920
+ import { pathToFileURL } from "url";
921
+ var generation = 0;
922
+ async function loadRoutesFrom(app, router, file) {
923
+ const url = `${pathToFileURL(file).href}?oriel-routes=${++generation}`;
924
+ const module = await app.run(() => import(
925
+ /* @vite-ignore */
926
+ url
927
+ ));
928
+ if (typeof module.default === "function") await app.run(() => module.default(router));
929
+ }
930
+ function healthResponse() {
931
+ const html2 = '<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<title>Application up</title>\n</head>\n<body><h1>Application up</h1></body>\n</html>';
932
+ return new Response(html2, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } });
933
+ }
934
+
935
+ // RoutingServiceProvider.ts
936
+ import { Router as Router2, RoutingPaths, UrlGenerator as UrlGenerator2 } from "@orieljs/contracts";
937
+ import { ServiceProvider } from "@orieljs/support";
938
+ var RoutingServiceProvider = class extends ServiceProvider {
939
+ register() {
940
+ this.app.singleton(Router2, () => new BaseRouter(this.app));
941
+ this.app.singleton(UrlGenerator2, () => {
942
+ const router = this.app.make(Router2);
943
+ if (!(router instanceof BaseRouter)) {
944
+ throw new RoutingError("The UrlGenerator needs the BaseRouter: the bound Router was replaced by another implementation.");
945
+ }
946
+ return new BaseUrlGenerator(router, this.app);
947
+ });
948
+ }
949
+ /**
950
+ * The files load in a booted() callback, after every provider's boot() (Laravel's withRouting does the same), so
951
+ * what the app's own providers set up at boot (Route.pattern/bind/model, gates, shared view data) is in place
952
+ * when the route files run.
953
+ */
954
+ boot() {
955
+ this.app.booted(async () => {
956
+ if (!this.app.bound(RoutingPaths)) return;
957
+ const { web, api, health, apiPrefix } = this.app.make(RoutingPaths);
958
+ const router = this.app.make(Router2);
959
+ if (health !== void 0) router.get(health, () => healthResponse());
960
+ if (web !== void 0) await router.middleware("web").group(() => loadRoutesFrom(this.app, router, web));
961
+ if (api !== void 0) await router.prefix(apiPrefix).middleware("api").group(() => loadRoutesFrom(this.app, router, api));
962
+ });
963
+ }
964
+ };
965
+ export {
966
+ BaseRouter,
967
+ BaseUrlGenerator,
968
+ ControllerMethodNotFoundError,
969
+ GroupRegistrar,
970
+ MethodNotAllowedHttpError,
971
+ MiddlewareNotFoundError,
972
+ NotFoundHttpError,
973
+ Route,
974
+ RouteDefinition,
975
+ RouteModelNotFoundError,
976
+ RouteNotDefinedError,
977
+ RoutingError,
978
+ RoutingServiceProvider,
979
+ UrlGenerationError,
980
+ buildQuery,
981
+ compareRoutes,
982
+ dispatchRoute,
983
+ healthResponse,
984
+ loadRoutesFrom,
985
+ mergeGroup,
986
+ normalizePath,
987
+ normalizeUri,
988
+ orderForBindings,
989
+ parseParameters,
990
+ renderError,
991
+ resolveMiddleware,
992
+ resourceRoutes,
993
+ runMiddleware,
994
+ substituteBindings,
995
+ toResponse
996
+ };
997
+ //# sourceMappingURL=index.js.map