routup 6.0.0-beta.2 → 6.0.0-beta.4

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.
@@ -1,4 +1,4 @@
1
- import { $ as AppOptions, $t as HandlerType, A as sendFormat, At as NodeHandler, B as setResponseHeaderAttachment, Bt as HandlerAfterListener, C as getRequestAcceptableContentTypes, Ct as isWebHandler, D as setResponseContentTypeByFileName, Dt as WebHandlerProvider, E as toResponse, Et as WebHandler, F as SendFileStats, Ft as Handler, G as EventStreamHandle, Gt as PathMatcher, H as appendResponseHeader, Ht as HandlerBeforeListener, I as sendFile, It as HandlerOptions, J as EventStreamListener, Jt as PathMatcherExecResult, K as EventStreamOptions, Kt as IPathMatcher, L as sendCreated, Lt as defineErrorHandler, M as SendFileContentOptions, Mt as defineCoreHandler, N as SendFileDisposition, Nt as CoreHandler, O as sendStream, Ot as fromNodeHandler, P as SendFileOptions, Pt as CoreHandlerOptions, Q as AppContext, Qt as HandlerSymbol, R as sendAccepted, Rt as ErrorHandler, S as getRequestAcceptableContentType, St as isHandlerOptions, T as isRequestCacheable, Tt as fromWebHandler, U as appendResponseHeaderDirective, Ut as HandlerErrorListener, V as setResponseHeaderInline, Vt as HandlerBaseOptions, W as serializeEventStreamMessage, Wt as isPath, X as ResponseCacheHeadersOptions, Xt as isError, Y as EventStreamMessage, Yt as PathMatcherOptions, Z as setResponseCacheHeaders, Zt as createError, _ as getRequestAcceptableLanguages, _t as isPluginError, a as SmartRouter, an as AppRequest, at as Route, b as getRequestAcceptableCharset, bt as matchHandlerMethod, c as RequestProtocolOptions, cn as NextFn, ct as LruCacheOptions, d as RequestIpOptions, dn as MethodNameLike, dt as Plugin, en as DispatcherEvent, et as AppOptionsInput, f as getRequestIP, fn as AppError, ft as PluginInstallContext, g as getRequestAcceptableLanguage, gt as PluginAlreadyInstalledError, h as matchRequestContentType, ht as PluginInstallError, i as TrieRouter, in as AppEventCreateContext, it as ObjectLiteral, j as SendFileContent, jt as NodeMiddleware, k as sendRedirect, kt as fromNodeMiddleware, l as getRequestProtocol, ln as HeaderName, lt as ICache, m as getRequestHostName, mn as HTTPErrorInput, mt as PluginNotInstalledError, n as App, nn as IDispatcherEvent, nt as BaseRouterOptions, o as SmartRouterOptions, on as AppResponse, ot as RouteMatch, p as RequestHostNameOptions, pn as ErrorSymbol, pt as PluginInstallFn, q as createEventStream, qt as Path, r as buildRoutePathMatcher, rn as AppEvent, rt as IRouter, s as LinearRouter, sn as IAppEvent, st as LruCache, t as normalizeAppOptions, tn as IDispatcher, tt as IApp, u as useRequestNegotiator, un as MethodName, ut as isPlugin, v as getRequestAcceptableEncoding, vt as PluginError, w as getRequestHeader, wt as isWebHandlerProvider, x as getRequestAcceptableCharsets, xt as isHandler, y as getRequestAcceptableEncodings, yt as PluginErrorCode, z as setResponseHeaderContentType, zt as ErrorHandlerOptions } from "./index-6qjxyFZh.mjs";
1
+ import { $ as AppOptions, $t as DispatcherEvent, A as sendFormat, At as NodeMiddleware, B as setResponseHeaderAttachment, Bt as HandlerBaseOptions, C as getRequestAcceptableContentTypes, Ct as isWebHandlerProvider, D as setResponseContentTypeByFileName, Dt as fromNodeHandler, E as toResponse, Et as WebHandlerProvider, F as SendFileStats, Ft as HandlerOptions, G as EventStreamHandle, Gt as IPathMatcher, H as appendResponseHeader, Ht as HandlerErrorListener, I as sendFile, It as defineErrorHandler, J as EventStreamListener, Jt as PathMatcherOptions, K as EventStreamOptions, Kt as Path, L as sendCreated, Lt as ErrorHandler, M as SendFileContentOptions, Mt as CoreHandler, N as SendFileDisposition, Nt as CoreHandlerOptions, O as sendStream, Ot as fromNodeMiddleware, P as SendFileOptions, Pt as Handler, Q as AppContext, Qt as HandlerType, R as sendAccepted, Rt as ErrorHandlerOptions, S as getRequestAcceptableContentType, St as isWebHandler, T as isRequestCacheable, Tt as WebHandler, U as appendResponseHeaderDirective, Ut as isPath, V as setResponseHeaderInline, Vt as HandlerBeforeListener, W as serializeEventStreamMessage, Wt as PathMatcher, X as ResponseCacheHeadersOptions, Xt as createError, Y as EventStreamMessage, Yt as isError, Z as setResponseCacheHeaders, Zt as HandlerSymbol, _ as getRequestAcceptableLanguages, _t as PluginError, a as SmartRouter, an as AppResponse, at as Route, b as getRequestAcceptableCharset, bt as isHandler, c as RequestProtocolOptions, cn as HeaderName, ct as LruCacheOptions, d as RequestIpOptions, dn as AppError, dt as Plugin, en as IDispatcher, et as AppOptionsInput, f as getRequestIP, fn as ErrorSymbol, ft as PluginInstallContext, g as getRequestAcceptableLanguage, gt as isPluginError, h as matchRequestContentType, ht as PluginInstallError, i as TrieRouter, in as AppRequest, it as ObjectLiteral, j as SendFileContent, jt as defineCoreHandler, k as sendRedirect, kt as NodeHandler, l as getRequestProtocol, ln as MethodName, lt as ICache, m as getRequestHostName, mt as PluginNotInstalledError, n as App, nn as AppEvent, nt as BaseRouterOptions, o as SmartRouterOptions, on as IAppEvent, ot as RouteMatch, p as RequestHostNameOptions, pn as HTTPErrorInput, pt as PluginInstallFn, q as createEventStream, qt as PathMatcherExecResult, r as buildRoutePathMatcher, rn as AppEventCreateContext, rt as IRouter, s as LinearRouter, sn as NextFn, st as LruCache, t as normalizeAppOptions, tn as IDispatcherEvent, tt as IApp, u as useRequestNegotiator, un as MethodNameLike, ut as isPlugin, v as getRequestAcceptableEncoding, vt as PluginErrorCode, w as getRequestHeader, wt as fromWebHandler, x as getRequestAcceptableCharsets, xt as isHandlerOptions, y as getRequestAcceptableEncodings, yt as matchHandlerMethod, z as setResponseHeaderContentType, zt as HandlerAfterListener } from "./index-colifb1o.mjs";
2
2
  import * as _$srvx from "srvx";
3
3
  import { ServerOptions } from "srvx";
4
4
  import * as _$srvx_service_worker0 from "srvx/service-worker";
@@ -6,5 +6,5 @@ import * as _$srvx_service_worker0 from "srvx/service-worker";
6
6
  //#region src/_entries/service-worker.d.ts
7
7
  declare function serve(app: IApp, options?: Omit<ServerOptions, 'fetch'>): _$srvx.Server<_$srvx_service_worker0.ServiceWorkerHandler>;
8
8
  //#endregion
9
- export { App, AppContext, AppError, AppEvent, AppEventCreateContext, AppOptions, AppOptionsInput, AppRequest, AppResponse, BaseRouterOptions, CoreHandler, CoreHandlerOptions, DispatcherEvent, ErrorHandler, ErrorHandlerOptions, ErrorSymbol, EventStreamHandle, EventStreamListener, EventStreamMessage, EventStreamOptions, HTTPErrorInput, Handler, HandlerAfterListener, HandlerBaseOptions, HandlerBeforeListener, HandlerErrorListener, HandlerOptions, HandlerSymbol, HandlerType, HeaderName, IApp, IAppEvent, ICache, IDispatcher, IDispatcherEvent, IPathMatcher, IRouter, LinearRouter, LruCache, LruCacheOptions, MethodName, MethodNameLike, NextFn, NodeHandler, NodeMiddleware, ObjectLiteral, Path, PathMatcher, PathMatcherExecResult, PathMatcherOptions, Plugin, PluginAlreadyInstalledError, PluginError, PluginErrorCode, PluginInstallContext, PluginInstallError, PluginInstallFn, PluginNotInstalledError, RequestHostNameOptions, RequestIpOptions, RequestProtocolOptions, ResponseCacheHeadersOptions, Route, RouteMatch, SendFileContent, SendFileContentOptions, SendFileDisposition, SendFileOptions, SendFileStats, SmartRouter, SmartRouterOptions, TrieRouter, WebHandler, WebHandlerProvider, appendResponseHeader, appendResponseHeaderDirective, buildRoutePathMatcher, createError, createEventStream, defineCoreHandler, defineErrorHandler, fromNodeHandler, fromNodeMiddleware, fromWebHandler, getRequestAcceptableCharset, getRequestAcceptableCharsets, getRequestAcceptableContentType, getRequestAcceptableContentTypes, getRequestAcceptableEncoding, getRequestAcceptableEncodings, getRequestAcceptableLanguage, getRequestAcceptableLanguages, getRequestHeader, getRequestHostName, getRequestIP, getRequestProtocol, isError, isHandler, isHandlerOptions, isPath, isPlugin, isPluginError, isRequestCacheable, isWebHandler, isWebHandlerProvider, matchHandlerMethod, matchRequestContentType, normalizeAppOptions, sendAccepted, sendCreated, sendFile, sendFormat, sendRedirect, sendStream, serializeEventStreamMessage, serve, setResponseCacheHeaders, setResponseContentTypeByFileName, setResponseHeaderAttachment, setResponseHeaderContentType, setResponseHeaderInline, toResponse, useRequestNegotiator };
9
+ export { App, AppContext, AppError, AppEvent, AppEventCreateContext, AppOptions, AppOptionsInput, AppRequest, AppResponse, BaseRouterOptions, CoreHandler, CoreHandlerOptions, DispatcherEvent, ErrorHandler, ErrorHandlerOptions, ErrorSymbol, EventStreamHandle, EventStreamListener, EventStreamMessage, EventStreamOptions, HTTPErrorInput, Handler, HandlerAfterListener, HandlerBaseOptions, HandlerBeforeListener, HandlerErrorListener, HandlerOptions, HandlerSymbol, HandlerType, HeaderName, IApp, IAppEvent, ICache, IDispatcher, IDispatcherEvent, IPathMatcher, IRouter, LinearRouter, LruCache, LruCacheOptions, MethodName, MethodNameLike, NextFn, NodeHandler, NodeMiddleware, ObjectLiteral, Path, PathMatcher, PathMatcherExecResult, PathMatcherOptions, Plugin, PluginError, PluginErrorCode, PluginInstallContext, PluginInstallError, PluginInstallFn, PluginNotInstalledError, RequestHostNameOptions, RequestIpOptions, RequestProtocolOptions, ResponseCacheHeadersOptions, Route, RouteMatch, SendFileContent, SendFileContentOptions, SendFileDisposition, SendFileOptions, SendFileStats, SmartRouter, SmartRouterOptions, TrieRouter, WebHandler, WebHandlerProvider, appendResponseHeader, appendResponseHeaderDirective, buildRoutePathMatcher, createError, createEventStream, defineCoreHandler, defineErrorHandler, fromNodeHandler, fromNodeMiddleware, fromWebHandler, getRequestAcceptableCharset, getRequestAcceptableCharsets, getRequestAcceptableContentType, getRequestAcceptableContentTypes, getRequestAcceptableEncoding, getRequestAcceptableEncodings, getRequestAcceptableLanguage, getRequestAcceptableLanguages, getRequestHeader, getRequestHostName, getRequestIP, getRequestProtocol, isError, isHandler, isHandlerOptions, isPath, isPlugin, isPluginError, isRequestCacheable, isWebHandler, isWebHandlerProvider, matchHandlerMethod, matchRequestContentType, normalizeAppOptions, sendAccepted, sendCreated, sendFile, sendFormat, sendRedirect, sendStream, serializeEventStreamMessage, serve, setResponseCacheHeaders, setResponseContentTypeByFileName, setResponseHeaderAttachment, setResponseHeaderContentType, setResponseHeaderInline, toResponse, useRequestNegotiator };
10
10
  //# sourceMappingURL=service-worker.d.mts.map
@@ -1,4 +1,4 @@
1
- import { $ as isError, A as fromWebHandler, B as DispatcherEvent, C as getRequestAcceptableEncodings, D as matchHandlerMethod, E as isRequestCacheable, F as defineErrorHandler, G as getRequestAcceptableContentTypes, H as sendRedirect, I as defineCoreHandler, J as sendFile, K as useRequestNegotiator, L as Handler, M as isWebHandlerProvider, N as fromNodeHandler, O as isHandler, P as fromNodeMiddleware, Q as createError, R as HandlerSymbol, S as getRequestAcceptableEncoding, T as getRequestAcceptableCharsets, U as sendFormat, V as sendStream, W as getRequestAcceptableContentType, X as sendAccepted, Y as sendCreated, Z as toResponse, _ as getRequestIP, a as LinearRouter, at as appendResponseHeaderDirective, b as getRequestAcceptableLanguage, c as PluginNotInstalledError, ct as AppError, d as PluginError, dt as AppEvent, et as setResponseHeaderContentType, f as isPluginError, ft as HeaderName, g as getRequestProtocol, h as PathMatcher, i as TrieRouter, it as appendResponseHeader, j as isWebHandler, k as isHandlerOptions, l as PluginInstallError, lt as ErrorSymbol, m as isPath, mt as LruCache, n as normalizeAppOptions, nt as setResponseHeaderInline, o as buildRoutePathMatcher, ot as createEventStream, p as PluginErrorCode, pt as MethodName, q as getRequestHeader, r as SmartRouter, rt as setResponseContentTypeByFileName, s as isPlugin, st as serializeEventStreamMessage, t as App, tt as setResponseHeaderAttachment, u as PluginAlreadyInstalledError, ut as setResponseCacheHeaders, v as getRequestHostName, w as getRequestAcceptableCharset, x as getRequestAcceptableLanguages, y as matchRequestContentType, z as HandlerType } from "./src-BfsqxIfL.mjs";
1
+ import { $ as setResponseHeaderContentType, A as isWebHandler, B as sendStream, C as getRequestAcceptableCharset, D as isHandler, E as matchHandlerMethod, F as defineCoreHandler, G as useRequestNegotiator, H as sendFormat, I as Handler, J as sendCreated, K as getRequestHeader, L as HandlerSymbol, M as fromNodeHandler, N as fromNodeMiddleware, O as isHandlerOptions, P as defineErrorHandler, Q as isError, R as HandlerType, S as getRequestAcceptableEncodings, T as isRequestCacheable, U as getRequestAcceptableContentType, V as sendRedirect, W as getRequestAcceptableContentTypes, X as toResponse, Y as sendAccepted, Z as createError, _ as getRequestHostName, a as LinearRouter, at as createEventStream, b as getRequestAcceptableLanguages, c as PluginNotInstalledError, ct as ErrorSymbol, d as isPluginError, dt as HeaderName, et as setResponseHeaderAttachment, f as PluginErrorCode, ft as MethodName, g as getRequestIP, h as getRequestProtocol, i as TrieRouter, it as appendResponseHeaderDirective, j as isWebHandlerProvider, k as fromWebHandler, l as PluginInstallError, lt as setResponseCacheHeaders, m as PathMatcher, n as normalizeAppOptions, nt as setResponseContentTypeByFileName, o as buildRoutePathMatcher, ot as serializeEventStreamMessage, p as isPath, pt as LruCache, q as sendFile, r as SmartRouter, rt as appendResponseHeader, s as isPlugin, st as AppError, t as App, tt as setResponseHeaderInline, u as PluginError, ut as AppEvent, v as matchRequestContentType, w as getRequestAcceptableCharsets, x as getRequestAcceptableEncoding, y as getRequestAcceptableLanguage, z as DispatcherEvent } from "./src-DE8cCC5t.mjs";
2
2
  import { serve as serve$1 } from "srvx/service-worker";
3
3
  //#region src/_entries/service-worker.ts
4
4
  function serve(app, options) {
@@ -8,6 +8,6 @@ function serve(app, options) {
8
8
  });
9
9
  }
10
10
  //#endregion
11
- export { App, AppError, AppEvent, DispatcherEvent, ErrorSymbol, Handler, HandlerSymbol, HandlerType, HeaderName, LinearRouter, LruCache, MethodName, PathMatcher, PluginAlreadyInstalledError, PluginError, PluginErrorCode, PluginInstallError, PluginNotInstalledError, SmartRouter, TrieRouter, appendResponseHeader, appendResponseHeaderDirective, buildRoutePathMatcher, createError, createEventStream, defineCoreHandler, defineErrorHandler, fromNodeHandler, fromNodeMiddleware, fromWebHandler, getRequestAcceptableCharset, getRequestAcceptableCharsets, getRequestAcceptableContentType, getRequestAcceptableContentTypes, getRequestAcceptableEncoding, getRequestAcceptableEncodings, getRequestAcceptableLanguage, getRequestAcceptableLanguages, getRequestHeader, getRequestHostName, getRequestIP, getRequestProtocol, isError, isHandler, isHandlerOptions, isPath, isPlugin, isPluginError, isRequestCacheable, isWebHandler, isWebHandlerProvider, matchHandlerMethod, matchRequestContentType, normalizeAppOptions, sendAccepted, sendCreated, sendFile, sendFormat, sendRedirect, sendStream, serializeEventStreamMessage, serve, setResponseCacheHeaders, setResponseContentTypeByFileName, setResponseHeaderAttachment, setResponseHeaderContentType, setResponseHeaderInline, toResponse, useRequestNegotiator };
11
+ export { App, AppError, AppEvent, DispatcherEvent, ErrorSymbol, Handler, HandlerSymbol, HandlerType, HeaderName, LinearRouter, LruCache, MethodName, PathMatcher, PluginError, PluginErrorCode, PluginInstallError, PluginNotInstalledError, SmartRouter, TrieRouter, appendResponseHeader, appendResponseHeaderDirective, buildRoutePathMatcher, createError, createEventStream, defineCoreHandler, defineErrorHandler, fromNodeHandler, fromNodeMiddleware, fromWebHandler, getRequestAcceptableCharset, getRequestAcceptableCharsets, getRequestAcceptableContentType, getRequestAcceptableContentTypes, getRequestAcceptableEncoding, getRequestAcceptableEncodings, getRequestAcceptableLanguage, getRequestAcceptableLanguages, getRequestHeader, getRequestHostName, getRequestIP, getRequestProtocol, isError, isHandler, isHandlerOptions, isPath, isPlugin, isPluginError, isRequestCacheable, isWebHandler, isWebHandlerProvider, matchHandlerMethod, matchRequestContentType, normalizeAppOptions, sendAccepted, sendCreated, sendFile, sendFormat, sendRedirect, sendStream, serializeEventStreamMessage, serve, setResponseCacheHeaders, setResponseContentTypeByFileName, setResponseHeaderAttachment, setResponseHeaderContentType, setResponseHeaderInline, toResponse, useRequestNegotiator };
12
12
 
13
13
  //# sourceMappingURL=service-worker.mjs.map
@@ -1421,7 +1421,6 @@ function isPath(input) {
1421
1421
  const PluginErrorCode = {
1422
1422
  PLUGIN: "PLUGIN",
1423
1423
  NOT_INSTALLED: "PLUGIN_NOT_INSTALLED",
1424
- ALREADY_INSTALLED: "PLUGIN_ALREADY_INSTALLED",
1425
1424
  INSTALL: "PLUGIN_INSTALL"
1426
1425
  };
1427
1426
  //#endregion
@@ -1442,19 +1441,6 @@ var PluginError = class extends AppError {
1442
1441
  }
1443
1442
  };
1444
1443
  //#endregion
1445
- //#region src/plugin/error/sub/already-installed.ts
1446
- var PluginAlreadyInstalledError = class extends PluginError {
1447
- pluginName;
1448
- constructor(pluginName) {
1449
- super({
1450
- message: `Plugin "${pluginName}" is already installed on this router.`,
1451
- code: PluginErrorCode.ALREADY_INSTALLED
1452
- });
1453
- this.name = "PluginAlreadyInstalledError";
1454
- this.pluginName = pluginName;
1455
- }
1456
- };
1457
- //#endregion
1458
1444
  //#region src/plugin/error/sub/install.ts
1459
1445
  var PluginInstallError = class extends PluginError {
1460
1446
  pluginName;
@@ -2269,8 +2255,26 @@ function normalizeAppOptions(input) {
2269
2255
  const AppSymbol = Symbol.for("App");
2270
2256
  //#endregion
2271
2257
  //#region src/app/check.ts
2258
+ /**
2259
+ * Discriminate an `IApp` argument from handlers, plugins, and other
2260
+ * inputs `App.use()` accepts. Two-stage check:
2261
+ *
2262
+ * 1. Fast path: brand check (`hasInstanceof` against `AppSymbol`).
2263
+ * Hits for every instance of the bundled `App` class and any
2264
+ * subclass that calls `markInstanceof(this, AppSymbol)` — that's
2265
+ * the common case, so we want it to be a single property lookup
2266
+ * with no key-by-key probing.
2267
+ *
2268
+ * 2. Fallback: structural check for the `IApp` surface `flatten()`
2269
+ * reads at mount time (`fetch`, `routes`, `plugins`,
2270
+ * `pluginSingletons`). Lets any object implementing the `IApp`
2271
+ * contract — not just instances of the bundled `App` class — be
2272
+ * mounted via `app.use(child)`.
2273
+ */
2272
2274
  function isAppInstance(input) {
2273
- return hasInstanceof(input, AppSymbol);
2275
+ if (hasInstanceof(input, AppSymbol)) return true;
2276
+ if (!isObject(input)) return false;
2277
+ return typeof input.fetch === "function" && Array.isArray(input.routes) && isObject(input.plugins) && isObject(input.pluginSingletons);
2274
2278
  }
2275
2279
  //#endregion
2276
2280
  //#region src/app/module.ts
@@ -2322,17 +2326,35 @@ var App = class App {
2322
2326
  */
2323
2327
  _options;
2324
2328
  /**
2325
- * Registry of installed plugins (name version) on this App.
2329
+ * Registry of installed plugins on this App, keyed by plugin name
2330
+ * then by canonical mount key (the joined `this._path` +
2331
+ * install-time `path`, falling back to `'/'` for a root install).
2332
+ * Inner-map value is the plugin version (or `undefined`).
2333
+ *
2334
+ * Per-path keying lets `hasPluginAt` answer "is plugin X mounted at
2335
+ * /api?" precisely. By default `install()` is permissive and
2336
+ * appends — same `(name, key)` writes the latest version. Plugins
2337
+ * opt into deduplication via `singleton` (any-path) or
2338
+ * `singletonByPath` (same-path) flags.
2326
2339
  *
2327
- * Read by `use(otherApp)` (via the public `plugins` getter) so
2328
- * plugin registries merge into the parent at flatten time
2329
- * `parent.hasPlugin('foo')` then reflects plugins installed on
2330
- * apps mounted into it.
2340
+ * Read by `flatten()` when merging a child's registry into this
2341
+ * one, so `parent.hasPlugin('foo')` reflects plugins installed on
2342
+ * mounted children too.
2331
2343
  *
2332
2344
  * @protected
2333
2345
  */
2334
2346
  _plugins;
2335
2347
  /**
2348
+ * Names of plugins installed with `singleton: true`. Re-installing
2349
+ * any of these names — even at a different mount path — is a
2350
+ * silent no-op. The claim is sticky: once a name is in here, it
2351
+ * stays for the lifetime of the App. Propagated through
2352
+ * `flatten()` so a child's singleton claim survives the mount.
2353
+ *
2354
+ * @protected
2355
+ */
2356
+ _pluginSingletons;
2357
+ /**
2336
2358
  * Every route registered on this App, in registration order.
2337
2359
  *
2338
2360
  * Read by `use(otherApp)` to snapshot routes at flatten time.
@@ -2343,6 +2365,7 @@ var App = class App {
2343
2365
  this.name = input.name;
2344
2366
  this._path = input.path;
2345
2367
  this._plugins = /* @__PURE__ */ new Map();
2368
+ this._pluginSingletons = /* @__PURE__ */ new Set();
2346
2369
  this.router = input.router ?? new LinearRouter();
2347
2370
  this._options = Object.freeze(normalizeAppOptions(input.options ?? {}));
2348
2371
  markInstanceof(this, AppSymbol);
@@ -2356,15 +2379,31 @@ var App = class App {
2356
2379
  return this._routes;
2357
2380
  }
2358
2381
  /**
2359
- * Public read of the installed-plugin registry. Used by
2360
- * `use(child)` to merge child plugins into the parent at
2361
- * flatten time. Returned as `ReadonlyMap` — callers must not
2362
- * mutate; go through `use(plugin)` to install.
2382
+ * Public read of the installed-plugin registry. Used by `flatten()`
2383
+ * to merge a child's plugins into this App without reaching into
2384
+ * the child's protected fields.
2385
+ *
2386
+ * Outer key: plugin name. Inner key: canonical mount path (`'/'`
2387
+ * for root mounts). Inner value: installed version (or `undefined`).
2388
+ *
2389
+ * Returned as nested `ReadonlyMap` — callers must not mutate; go
2390
+ * through `app.use(plugin)` to install.
2363
2391
  */
2364
2392
  get plugins() {
2365
2393
  return this._plugins;
2366
2394
  }
2367
2395
  /**
2396
+ * Public read of the sticky singleton-claim set. Once a plugin
2397
+ * name is claimed singleton on an App, every subsequent install
2398
+ * of that name is a silent no-op. Used by `flatten()` to
2399
+ * propagate child claims forward at mount time.
2400
+ *
2401
+ * Returned as `ReadonlySet` — callers must not mutate.
2402
+ */
2403
+ get pluginSingletons() {
2404
+ return this._pluginSingletons;
2405
+ }
2406
+ /**
2368
2407
  * Register a route with the active router and record it on the
2369
2408
  * App so `setRouter` / `use(child)` can read the canonical list
2370
2409
  * back.
@@ -2452,7 +2491,7 @@ var App = class App {
2452
2491
  let response;
2453
2492
  try {
2454
2493
  const matches = this.router.lookup(event.path, event.method);
2455
- response = await this.runMatches(event, matches, event.path, 0);
2494
+ response = await this.runMatches(event, matches, 0);
2456
2495
  if (!event.error && !event.dispatched && isRoot && event.method === MethodName.OPTIONS) {
2457
2496
  if (event.methodsAllowed.has(MethodName.GET)) event.methodsAllowed.add(MethodName.HEAD);
2458
2497
  const options = [...event.methodsAllowed].map((key) => key.toUpperCase()).join(",");
@@ -2479,8 +2518,16 @@ var App = class App {
2479
2518
  * Walk the matched routes for the current event, dispatching each
2480
2519
  * handler in order. Re-entered (recursively) from the `setNext`
2481
2520
  * continuation so `event.next()` resumes from the next match.
2521
+ *
2522
+ * The match list is captured once per dispatch — there is no
2523
+ * mid-walk path-rewrite refresh. `IAppEvent.path` is a snapshot
2524
+ * on the handler's facade event (see `event/module.ts`), so user
2525
+ * middleware cannot mutate the dispatcher's path before calling
2526
+ * `event.next()`. If a future API surface lets middleware rewrite
2527
+ * paths end-to-end, this loop will need a per-call refresh + a
2528
+ * `methodsAllowed` reset (see closed issue #913).
2482
2529
  */
2483
- async runMatches(event, matches, matchesPath, startIndex) {
2530
+ async runMatches(event, matches, startIndex) {
2484
2531
  let i = startIndex;
2485
2532
  let response;
2486
2533
  while (!event.dispatched && i < matches.length) {
@@ -2500,15 +2547,10 @@ var App = class App {
2500
2547
  const savedMountPath = event.mountPath;
2501
2548
  if (typeof match.path === "string") event.mountPath = match.path;
2502
2549
  const capturedMatches = matches;
2503
- const capturedMatchesPath = matchesPath;
2504
2550
  const nextIndex = i + 1;
2505
2551
  event.setNext(async (error) => {
2506
2552
  if (error) event.error = createError(error);
2507
- const pathChanged = event.path !== capturedMatchesPath;
2508
- const nextMatches = pathChanged ? this.router.lookup(event.path, event.method) : capturedMatches;
2509
- const nextMatchesPath = pathChanged ? event.path : capturedMatchesPath;
2510
- const nextStart = pathChanged ? 0 : nextIndex;
2511
- return this.runMatches(event, nextMatches, nextMatchesPath, nextStart);
2553
+ return this.runMatches(event, capturedMatches, nextIndex);
2512
2554
  });
2513
2555
  try {
2514
2556
  const dispatchResponse = await handler.dispatch(event);
@@ -2618,38 +2660,104 @@ var App = class App {
2618
2660
  * @protected
2619
2661
  */
2620
2662
  flatten(child, path) {
2621
- for (const name of child.plugins.keys()) if (this._plugins.has(name)) throw new PluginAlreadyInstalledError(name);
2622
- for (const [name, version] of child.plugins) this._plugins.set(name, version);
2623
2663
  for (const route of child.routes) this.register({
2624
2664
  path: joinPaths(this._path, path, route.path),
2625
2665
  method: route.method,
2626
2666
  data: route.data
2627
2667
  });
2668
+ const namesBeforeMerge = new Set(this._plugins.keys());
2669
+ for (const [name, childPaths] of child.plugins) {
2670
+ if (this._pluginSingletons.has(name)) continue;
2671
+ let entry = this._plugins.get(name);
2672
+ for (const [childKey, version] of childPaths) {
2673
+ const composedKey = joinPaths(this._path, path, childKey) ?? "/";
2674
+ if (entry && entry.has(composedKey)) continue;
2675
+ if (!entry) {
2676
+ entry = /* @__PURE__ */ new Map();
2677
+ this._plugins.set(name, entry);
2678
+ }
2679
+ entry.set(composedKey, version);
2680
+ }
2681
+ }
2682
+ for (const name of child.pluginSingletons) if (!namesBeforeMerge.has(name)) this._pluginSingletons.add(name);
2628
2683
  }
2629
2684
  /**
2630
- * Check if a plugin with the given name is installed on this App.
2685
+ * Check if a plugin with the given name is installed on this App at
2686
+ * *any* mount path.
2631
2687
  */
2632
2688
  hasPlugin(name) {
2633
- return this._plugins.has(name);
2689
+ const entry = this._plugins.get(name);
2690
+ return !!entry && entry.size > 0;
2691
+ }
2692
+ /**
2693
+ * Check if a plugin with the given name is installed at the given
2694
+ * install-time `path`. `path` is interpreted the same way as the
2695
+ * argument to `app.use(path, plugin)` — relative to this App. Omit
2696
+ * `path` to check the root install.
2697
+ */
2698
+ hasPluginAt(name, path) {
2699
+ const entry = this._plugins.get(name);
2700
+ if (!entry) return false;
2701
+ const key = joinPaths(this._path, path) ?? "/";
2702
+ return entry.has(key);
2634
2703
  }
2635
2704
  /**
2636
2705
  * Get the version of an installed plugin by name, or `undefined`
2637
- * if the plugin is not installed.
2706
+ * when the plugin is not installed. When the plugin is mounted at
2707
+ * several paths, returns the version of an arbitrary mount —
2708
+ * typical usage installs the same plugin object at every mount, so
2709
+ * the version is identical. Use `getPluginVersionAt` to read the
2710
+ * version of a specific mount.
2638
2711
  */
2639
2712
  getPluginVersion(name) {
2640
- return this._plugins.get(name);
2713
+ const entry = this._plugins.get(name);
2714
+ if (!entry) return;
2715
+ const first = entry.values().next();
2716
+ return first.done ? void 0 : first.value;
2717
+ }
2718
+ /**
2719
+ * Get the version of a plugin installed at the given install-time
2720
+ * `path`, or `undefined` when no install matches. `path` is
2721
+ * interpreted relative to this App (same convention as
2722
+ * `app.use(path, plugin)`); omit it to read the root install.
2723
+ */
2724
+ getPluginVersionAt(name, path) {
2725
+ const entry = this._plugins.get(name);
2726
+ if (!entry) return;
2727
+ return entry.get(joinPaths(this._path, path) ?? "/");
2728
+ }
2729
+ /**
2730
+ * List every canonical mount path the named plugin is installed
2731
+ * at. Returns an empty array when the plugin is not installed.
2732
+ * Each path is the joined `app._path` + install-time path,
2733
+ * normalized to `'/'` for root mounts.
2734
+ */
2735
+ getPluginMountPaths(name) {
2736
+ const entry = this._plugins.get(name);
2737
+ if (!entry) return [];
2738
+ return Array.from(entry.keys());
2641
2739
  }
2642
2740
  install(plugin, context = {}) {
2643
- if (this._plugins.has(plugin.name)) throw new PluginAlreadyInstalledError(plugin.name);
2741
+ const mountKey = joinPaths(this._path, context.path) ?? "/";
2742
+ const existing = this._plugins.get(plugin.name);
2743
+ if (this._pluginSingletons.has(plugin.name)) return this;
2744
+ if (plugin.singleton && existing && existing.size > 0) return this;
2745
+ if (plugin.singletonByPath && existing && existing.has(mountKey)) return this;
2644
2746
  const scratch = new App({ name: plugin.name });
2645
2747
  plugin.install(scratch);
2646
2748
  if (context.path) this.use(context.path, scratch);
2647
2749
  else this.use(scratch);
2648
- this._plugins.set(plugin.name, plugin.version);
2750
+ let entry = this._plugins.get(plugin.name);
2751
+ if (!entry) {
2752
+ entry = /* @__PURE__ */ new Map();
2753
+ this._plugins.set(plugin.name, entry);
2754
+ }
2755
+ entry.set(mountKey, plugin.version);
2756
+ if (plugin.singleton) this._pluginSingletons.add(plugin.name);
2649
2757
  return this;
2650
2758
  }
2651
2759
  };
2652
2760
  //#endregion
2653
- export { isError as $, fromWebHandler as A, DispatcherEvent as B, getRequestAcceptableEncodings as C, matchHandlerMethod as D, isRequestCacheable as E, defineErrorHandler as F, getRequestAcceptableContentTypes as G, sendRedirect as H, defineCoreHandler as I, sendFile as J, useRequestNegotiator as K, Handler as L, isWebHandlerProvider as M, fromNodeHandler as N, isHandler as O, fromNodeMiddleware as P, createError as Q, HandlerSymbol as R, getRequestAcceptableEncoding as S, getRequestAcceptableCharsets as T, sendFormat as U, sendStream as V, getRequestAcceptableContentType as W, sendAccepted as X, sendCreated as Y, toResponse as Z, getRequestIP as _, LinearRouter as a, appendResponseHeaderDirective as at, getRequestAcceptableLanguage as b, PluginNotInstalledError as c, AppError as ct, PluginError as d, AppEvent as dt, setResponseHeaderContentType as et, isPluginError as f, HeaderName as ft, getRequestProtocol as g, PathMatcher as h, TrieRouter as i, appendResponseHeader as it, isWebHandler as j, isHandlerOptions as k, PluginInstallError as l, ErrorSymbol as lt, isPath as m, LruCache as mt, normalizeAppOptions as n, setResponseHeaderInline as nt, buildRoutePathMatcher as o, createEventStream as ot, PluginErrorCode as p, MethodName as pt, getRequestHeader as q, SmartRouter as r, setResponseContentTypeByFileName as rt, isPlugin as s, serializeEventStreamMessage as st, App as t, setResponseHeaderAttachment as tt, PluginAlreadyInstalledError as u, setResponseCacheHeaders as ut, getRequestHostName as v, getRequestAcceptableCharset as w, getRequestAcceptableLanguages as x, matchRequestContentType as y, HandlerType as z };
2761
+ export { setResponseHeaderContentType as $, isWebHandler as A, sendStream as B, getRequestAcceptableCharset as C, isHandler as D, matchHandlerMethod as E, defineCoreHandler as F, useRequestNegotiator as G, sendFormat as H, Handler as I, sendCreated as J, getRequestHeader as K, HandlerSymbol as L, fromNodeHandler as M, fromNodeMiddleware as N, isHandlerOptions as O, defineErrorHandler as P, isError as Q, HandlerType as R, getRequestAcceptableEncodings as S, isRequestCacheable as T, getRequestAcceptableContentType as U, sendRedirect as V, getRequestAcceptableContentTypes as W, toResponse as X, sendAccepted as Y, createError as Z, getRequestHostName as _, LinearRouter as a, createEventStream as at, getRequestAcceptableLanguages as b, PluginNotInstalledError as c, ErrorSymbol as ct, isPluginError as d, HeaderName as dt, setResponseHeaderAttachment as et, PluginErrorCode as f, MethodName as ft, getRequestIP as g, getRequestProtocol as h, TrieRouter as i, appendResponseHeaderDirective as it, isWebHandlerProvider as j, fromWebHandler as k, PluginInstallError as l, setResponseCacheHeaders as lt, PathMatcher as m, normalizeAppOptions as n, setResponseContentTypeByFileName as nt, buildRoutePathMatcher as o, serializeEventStreamMessage as ot, isPath as p, LruCache as pt, sendFile as q, SmartRouter as r, appendResponseHeader as rt, isPlugin as s, AppError as st, App as t, setResponseHeaderInline as tt, PluginError as u, AppEvent as ut, matchRequestContentType as v, getRequestAcceptableCharsets as w, getRequestAcceptableEncoding as x, getRequestAcceptableLanguage as y, DispatcherEvent as z };
2654
2762
 
2655
- //# sourceMappingURL=src-BfsqxIfL.mjs.map
2763
+ //# sourceMappingURL=src-DE8cCC5t.mjs.map