@nestjs/core 11.2.1 → 11.2.3

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.
@@ -0,0 +1,10 @@
1
+ type InstanceDecorator = (target: unknown) => unknown;
2
+ /**
3
+ * Wraps an `instrument.instanceDecorator` so that a decorator throwing on a
4
+ * given instance (e.g. when inspecting a Proxy whose traps throw outside of
5
+ * their intended context, such as `nestjs-cls` proxy providers) does not
6
+ * crash the application bootstrap. The original, undecorated instance is
7
+ * used instead and a warning is logged.
8
+ */
9
+ export declare function makeSafeInstanceDecorator(decorator: InstanceDecorator): InstanceDecorator;
10
+ export {};
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeSafeInstanceDecorator = makeSafeInstanceDecorator;
4
+ const common_1 = require("@nestjs/common");
5
+ const logger = new common_1.Logger('InstrumentLogger');
6
+ /**
7
+ * Wraps an `instrument.instanceDecorator` so that a decorator throwing on a
8
+ * given instance (e.g. when inspecting a Proxy whose traps throw outside of
9
+ * their intended context, such as `nestjs-cls` proxy providers) does not
10
+ * crash the application bootstrap. The original, undecorated instance is
11
+ * used instead and a warning is logged.
12
+ */
13
+ function makeSafeInstanceDecorator(decorator) {
14
+ return (target) => {
15
+ try {
16
+ return decorator(target);
17
+ }
18
+ catch (err) {
19
+ logger.warn(`The "instanceDecorator" function threw an error while decorating an instance (${err?.message ?? err}). The undecorated instance will be used instead.`);
20
+ return target;
21
+ }
22
+ };
23
+ }
@@ -12,6 +12,7 @@ const runtime_exception_1 = require("../errors/exceptions/runtime.exception");
12
12
  const undefined_dependency_exception_1 = require("../errors/exceptions/undefined-dependency.exception");
13
13
  const unknown_dependencies_exception_1 = require("../errors/exceptions/unknown-dependencies.exception");
14
14
  const barrier_1 = require("../helpers/barrier");
15
+ const safe_instance_decorator_1 = require("../helpers/safe-instance-decorator");
15
16
  const constants_2 = require("./constants");
16
17
  const inquirer_1 = require("./inquirer");
17
18
  const instance_wrapper_1 = require("./instance-wrapper");
@@ -22,7 +23,7 @@ class Injector {
22
23
  this.logger = new common_1.Logger('InjectorLogger');
23
24
  this.instanceDecorator = (target) => target;
24
25
  if (options?.instanceDecorator) {
25
- this.instanceDecorator = options.instanceDecorator;
26
+ this.instanceDecorator = (0, safe_instance_decorator_1.makeSafeInstanceDecorator)(options.instanceDecorator);
26
27
  }
27
28
  }
28
29
  loadPrototype({ token }, collection, contextId = constants_2.STATIC_CONTEXT) {
@@ -266,12 +267,23 @@ class Injector {
266
267
  * that eventual lazily created instance will be merged with the prototype
267
268
  * instantiated beforehand.
268
269
  */
269
- instanceHost.donePromise &&
270
+ if (instanceHost.donePromise) {
270
271
  void instanceHost.donePromise
271
272
  .then(() => this.loadProvider(instanceWrapper, moduleRef, resolutionContext))
272
273
  .catch(err => {
273
274
  instanceWrapper.settlementSignal?.error(err);
274
275
  });
276
+ }
277
+ else {
278
+ /**
279
+ * No load has ever been scheduled for this context (e.g., request-scoped
280
+ * providers are no longer instantiated during static bootstrap, so a fresh
281
+ * durable/request sub-tree host has no inherited `donePromise`).
282
+ * Load it now; if a circular dependency is truly in-flight, the nested
283
+ * lookup will find this host pending and defer through its `donePromise`.
284
+ */
285
+ await this.loadProvider(instanceWrapper, instanceWrapper.host ?? moduleRef, resolutionContext);
286
+ }
275
287
  }
276
288
  if (instanceWrapper.async) {
277
289
  const host = instanceWrapper.getInstanceByContextId(this.getContextId(resolutionContext.contextId, instanceWrapper), inquirerId);
@@ -11,6 +11,7 @@ const exceptions_1 = require("../errors/exceptions");
11
11
  const context_id_factory_1 = require("../helpers/context-id-factory");
12
12
  const get_class_scope_1 = require("../helpers/get-class-scope");
13
13
  const is_durable_1 = require("../helpers/is-durable");
14
+ const safe_instance_decorator_1 = require("../helpers/safe-instance-decorator");
14
15
  const uuid_factory_1 = require("../inspector/uuid-factory");
15
16
  const constants_2 = require("./constants");
16
17
  const instance_wrapper_1 = require("./instance-wrapper");
@@ -252,7 +253,9 @@ class Module {
252
253
  token: providerToken,
253
254
  name: providerToken?.name || providerToken,
254
255
  metatype: null,
255
- instance: instanceDecorator ? instanceDecorator(value) : value,
256
+ instance: instanceDecorator
257
+ ? (0, safe_instance_decorator_1.makeSafeInstanceDecorator)(instanceDecorator)(value)
258
+ : value,
256
259
  isResolved: true,
257
260
  async: value instanceof Promise,
258
261
  host: this,
@@ -10,6 +10,7 @@ const os_1 = require("os");
10
10
  const application_config_1 = require("./application-config");
11
11
  const constants_1 = require("./constants");
12
12
  const optional_require_1 = require("./helpers/optional-require");
13
+ const safe_instance_decorator_1 = require("./helpers/safe-instance-decorator");
13
14
  const injector_1 = require("./injector/injector");
14
15
  const container_1 = require("./middleware/container");
15
16
  const middleware_module_1 = require("./middleware/middleware-module");
@@ -325,7 +326,8 @@ class NestApplication extends nest_application_context_1.NestApplicationContext
325
326
  }
326
327
  applyInstanceDecoratorIfRegistered(...instances) {
327
328
  if (this.appOptions.instrument?.instanceDecorator) {
328
- return instances.map(instance => this.appOptions.instrument.instanceDecorator(instance));
329
+ const decorate = (0, safe_instance_decorator_1.makeSafeInstanceDecorator)(this.appOptions.instrument.instanceDecorator);
330
+ return instances.map(instance => decorate(instance));
329
331
  }
330
332
  return instances;
331
333
  }
@@ -333,15 +335,21 @@ class NestApplication extends nest_application_context_1.NestApplicationContext
333
335
  if (!this.appOptions.instrument?.instanceDecorator) {
334
336
  return args;
335
337
  }
336
- const [firstArg, secondArg] = args;
337
- return [
338
- (0, shared_utils_1.isFunction)(firstArg)
339
- ? this.appOptions.instrument.instanceDecorator(firstArg)
340
- : firstArg,
341
- (0, shared_utils_1.isFunction)(secondArg)
342
- ? this.appOptions.instrument.instanceDecorator(secondArg)
343
- : secondArg,
344
- ];
338
+ const decorate = (0, safe_instance_decorator_1.makeSafeInstanceDecorator)(this.appOptions.instrument.instanceDecorator);
339
+ // Decorators may return a non-function value for plain middleware
340
+ // functions; fall back to the original argument so the HTTP adapter
341
+ // always receives a valid handler.
342
+ const decorateFunction = (arg) => {
343
+ if (!(0, shared_utils_1.isFunction)(arg)) {
344
+ return arg;
345
+ }
346
+ const decorated = decorate(arg);
347
+ return (0, shared_utils_1.isFunction)(decorated) ? decorated : arg;
348
+ };
349
+ // Map over the original arguments to preserve arity: appending a trailing
350
+ // `undefined` to a single-argument `use(fn)` call would make Express 5's
351
+ // router throw "argument handler must be a function".
352
+ return args.map(decorateFunction);
345
353
  }
346
354
  }
347
355
  exports.NestApplication = NestApplication;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/core",
3
- "version": "11.2.1",
3
+ "version": "11.2.3",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@core)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  "uid": "2.0.2"
29
29
  },
30
30
  "devDependencies": {
31
- "@nestjs/common": "11.2.1"
31
+ "@nestjs/common": "11.2.3"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@nestjs/common": "^11.0.0",
@@ -49,5 +49,5 @@
49
49
  "optional": true
50
50
  }
51
51
  },
52
- "gitHead": "4535f43b4890c9c69c57c5a5a8b49f62c83d1ed6"
52
+ "gitHead": "2b36ee5fea13dedcedfd9815a9c193b2d21130c1"
53
53
  }
@@ -1,4 +0,0 @@
1
- import { RuntimeException } from './runtime.exception.js';
2
- export declare class RouteConflictException extends RuntimeException {
3
- constructor(messages: string[]);
4
- }
@@ -1,7 +0,0 @@
1
- import { ROUTE_CONFLICT_MESSAGE } from '../messages.js';
2
- import { RuntimeException } from './runtime.exception.js';
3
- export class RouteConflictException extends RuntimeException {
4
- constructor(messages) {
5
- super(ROUTE_CONFLICT_MESSAGE(messages));
6
- }
7
- }
@@ -1,3 +0,0 @@
1
- import { InjectionToken } from '@nestjs/common';
2
- import { InstanceWrapper } from '../../injector/instance-wrapper.js';
3
- export declare function getInstancesGroupedByHierarchyLevel(...collections: Array<Map<InjectionToken, InstanceWrapper> | Array<[InjectionToken, InstanceWrapper]>>): Map<number, unknown[]>;
@@ -1,27 +0,0 @@
1
- export function getInstancesGroupedByHierarchyLevel(...collections) {
2
- const groupedByHierarchyLevel = new Map();
3
- for (const collection of collections) {
4
- for (const [_, wrapper] of collection) {
5
- if (!wrapper.isDependencyTreeStatic()) {
6
- continue;
7
- }
8
- const level = wrapper.hierarchyLevel;
9
- if (!groupedByHierarchyLevel.has(level)) {
10
- groupedByHierarchyLevel.set(level, []);
11
- }
12
- const byHierarchyLevelGroup = groupedByHierarchyLevel.get(level);
13
- if (wrapper.isTransient) {
14
- const staticTransientInstances = wrapper
15
- .getStaticTransientInstances()
16
- .filter(i => !!i)
17
- .map(i => i.instance);
18
- byHierarchyLevelGroup.push(...staticTransientInstances);
19
- continue;
20
- }
21
- if (wrapper.instance) {
22
- byHierarchyLevelGroup.push(wrapper.instance);
23
- }
24
- }
25
- }
26
- return groupedByHierarchyLevel;
27
- }
@@ -1 +0,0 @@
1
- export declare function getSortedHierarchyLevels(groups: Map<number, unknown[]>, order?: 'ASC' | 'DESC'): number[];
@@ -1,7 +0,0 @@
1
- export function getSortedHierarchyLevels(groups, order = 'ASC') {
2
- const comparator = order === 'ASC'
3
- ? (a, b) => a - b
4
- : (a, b) => b - a;
5
- const levels = Array.from(groups.keys()).sort(comparator);
6
- return levels;
7
- }
@@ -1,2 +0,0 @@
1
- export * from './module-definition.interface.js';
2
- export * from './module-override.interface.js';
@@ -1,2 +0,0 @@
1
- export * from './module-definition.interface.js';
2
- export * from './module-override.interface.js';
package/internal.d.ts DELETED
@@ -1,38 +0,0 @@
1
- /**
2
- * Internal module - not part of the public API.
3
- * These exports are used by sibling @nestjs packages.
4
- * Do not depend on these in your application code.
5
- * @internal
6
- * @module
7
- */
8
- export { RuntimeException } from './errors/exceptions/runtime.exception.js';
9
- export { InvalidExceptionFilterException } from './errors/exceptions/invalid-exception-filter.exception.js';
10
- export { MESSAGES } from './constants.js';
11
- export { DependenciesScanner } from './scanner.js';
12
- export { Injector, InjectorDependencyContext } from './injector/injector.js';
13
- export { InstanceLoader } from './injector/instance-loader.js';
14
- export { InstanceWrapper } from './injector/instance-wrapper.js';
15
- export { Module } from './injector/module.js';
16
- export { STATIC_CONTEXT } from './injector/constants.js';
17
- export { ExecutionContextHost } from './helpers/execution-context-host.js';
18
- export { ContextUtils, ParamProperties } from './helpers/context-utils.js';
19
- export { HandlerMetadataStorage } from './helpers/handler-metadata-storage.js';
20
- export { RouterMethodFactory } from './helpers/router-method-factory.js';
21
- export { loadAdapter } from './helpers/load-adapter.js';
22
- export { optionalRequire } from './helpers/optional-require.js';
23
- export { ParamsMetadata } from './helpers/interfaces/index.js';
24
- export { GuardsConsumer } from './guards/guards-consumer.js';
25
- export { GuardsContextCreator } from './guards/guards-context-creator.js';
26
- export { FORBIDDEN_MESSAGE } from './guards/constants.js';
27
- export { PipesConsumer } from './pipes/pipes-consumer.js';
28
- export { PipesContextCreator } from './pipes/pipes-context-creator.js';
29
- export { ParamsTokenFactory } from './pipes/params-token-factory.js';
30
- export { InterceptorsConsumer } from './interceptors/interceptors-consumer.js';
31
- export { InterceptorsContextCreator } from './interceptors/interceptors-context-creator.js';
32
- export { BaseExceptionFilterContext } from './exceptions/base-exception-filter-context.js';
33
- export { LegacyRouteConverter } from './router/legacy-route-converter.js';
34
- export { REQUEST_CONTEXT_ID } from './router/request/request-constants.js';
35
- export { NoopGraphInspector } from './inspector/noop-graph-inspector.js';
36
- export { UuidFactory, UuidFactoryMode } from './inspector/uuid-factory.js';
37
- export { ModuleDefinition } from './interfaces/module-definition.interface.js';
38
- export { ModuleOverride } from './interfaces/module-override.interface.js';
package/internal.js DELETED
@@ -1,46 +0,0 @@
1
- /**
2
- * Internal module - not part of the public API.
3
- * These exports are used by sibling @nestjs packages.
4
- * Do not depend on these in your application code.
5
- * @internal
6
- * @module
7
- */
8
- // Errors
9
- export { RuntimeException } from './errors/exceptions/runtime.exception.js';
10
- export { InvalidExceptionFilterException } from './errors/exceptions/invalid-exception-filter.exception.js';
11
- // Constants
12
- export { MESSAGES } from './constants.js';
13
- // Scanner
14
- export { DependenciesScanner } from './scanner.js';
15
- // Injector
16
- export { Injector } from './injector/injector.js';
17
- export { InstanceLoader } from './injector/instance-loader.js';
18
- export { InstanceWrapper } from './injector/instance-wrapper.js';
19
- export { Module } from './injector/module.js';
20
- export { STATIC_CONTEXT } from './injector/constants.js';
21
- // Helpers
22
- export { ExecutionContextHost } from './helpers/execution-context-host.js';
23
- export { ContextUtils } from './helpers/context-utils.js';
24
- export { HandlerMetadataStorage } from './helpers/handler-metadata-storage.js';
25
- export { RouterMethodFactory } from './helpers/router-method-factory.js';
26
- export { loadAdapter } from './helpers/load-adapter.js';
27
- export { optionalRequire } from './helpers/optional-require.js';
28
- // Guards
29
- export { GuardsConsumer } from './guards/guards-consumer.js';
30
- export { GuardsContextCreator } from './guards/guards-context-creator.js';
31
- export { FORBIDDEN_MESSAGE } from './guards/constants.js';
32
- // Pipes
33
- export { PipesConsumer } from './pipes/pipes-consumer.js';
34
- export { PipesContextCreator } from './pipes/pipes-context-creator.js';
35
- export { ParamsTokenFactory } from './pipes/params-token-factory.js';
36
- // Interceptors
37
- export { InterceptorsConsumer } from './interceptors/interceptors-consumer.js';
38
- export { InterceptorsContextCreator } from './interceptors/interceptors-context-creator.js';
39
- // Exceptions
40
- export { BaseExceptionFilterContext } from './exceptions/base-exception-filter-context.js';
41
- // Router
42
- export { LegacyRouteConverter } from './router/legacy-route-converter.js';
43
- export { REQUEST_CONTEXT_ID } from './router/request/request-constants.js';
44
- // Inspector
45
- export { NoopGraphInspector } from './inspector/noop-graph-inspector.js';
46
- export { UuidFactory, UuidFactoryMode } from './inspector/uuid-factory.js';
@@ -1,32 +0,0 @@
1
- import { RequestMethod } from '@nestjs/common';
2
- import { type VersionValue } from '@nestjs/common/internal';
3
- import { InstanceWrapper } from '../../injector/instance-wrapper.js';
4
- import { RouterProxyCallback } from '../router-proxy.js';
5
- /**
6
- * Loose callable signature shared by the various handler wrappers that
7
- * are composed before adapter registration (host filter, version
8
- * filter, request-scoped handler, etc.). They all accept the (req, res,
9
- * next) trio but may be invoked variadically by adapter shims.
10
- */
11
- export type ResolvedRouteHandler = (...args: unknown[]) => unknown;
12
- /**
13
- * Final route description produced during the "collect" phase of the
14
- * router pipeline and consumed during the "register" phase. Holds the
15
- * fully composed path, the pre-built handler chain (proxy + host filter
16
- * + optional version filter), and the metadata needed to register the
17
- * route on the HTTP adapter and to insert an entrypoint into the graph
18
- * inspector.
19
- */
20
- export interface ResolvedRoute {
21
- method: RequestMethod;
22
- path: string;
23
- rawPath?: string;
24
- host: string | RegExp | Array<string | RegExp> | undefined;
25
- version: VersionValue | undefined;
26
- methodVersion: VersionValue | undefined;
27
- controllerVersion: VersionValue | undefined;
28
- handler: ResolvedRouteHandler;
29
- targetCallback: RouterProxyCallback;
30
- methodName: string;
31
- instanceWrapper: InstanceWrapper;
32
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,14 +0,0 @@
1
- import { ResolvedRoute } from './resolved-route.interface.js';
2
- /**
3
- * Distinguishes the two flavors of route overlap.
4
- * - `duplicate` — identical method + path + version + host registered twice.
5
- * - `shadow` — patterns can match the same request but are not identical.
6
- */
7
- export type ConflictKind = 'duplicate' | 'shadow';
8
- export interface RouteConflict {
9
- /** Route registered first; on order-sensitive adapters this wins. */
10
- winner: ResolvedRoute;
11
- /** Route registered later; on order-sensitive adapters this never matches. */
12
- shadowed: ResolvedRoute;
13
- kind: ConflictKind;
14
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,24 +0,0 @@
1
- import { ResolvedRoute } from './resolved-route.interface.js';
2
- /**
3
- * Options that control how `Resolver.resolve` walks the controller
4
- * graph and registers routes on the HTTP adapter. Used internally to
5
- * thread route-collection and deferred-registration concerns through
6
- * the resolver chain without bloating individual method signatures.
7
- */
8
- export interface RouteResolutionOptions {
9
- /**
10
- * Invoked once for each route after its final path, host and version
11
- * have been composed. Lets the caller observe resolved routes (for
12
- * conflict detection, specificity sorting, etc.) without coupling
13
- * those concerns to the resolver itself.
14
- */
15
- onRouteResolved?: (route: ResolvedRoute) => void;
16
- /**
17
- * When `true`, the resolver still walks every controller and emits
18
- * `onRouteResolved` callbacks but skips the actual adapter
19
- * registration step. The caller is then responsible for ordering and
20
- * installing the collected routes via `registerResolvedRoute`.
21
- * Defaults to `false`.
22
- */
23
- deferRegistration?: boolean;
24
- }
@@ -1,71 +0,0 @@
1
- import { Logger, type RouteConflictPolicy, type VersioningOptions } from '@nestjs/common';
2
- import { ResolvedRoute } from './interfaces/resolved-route.interface.js';
3
- import { RouteConflict } from './interfaces/route-conflict.interface.js';
4
- type SegmentKind = 'literal' | 'param' | 'wildcard';
5
- interface PathSegment {
6
- kind: SegmentKind;
7
- value: string;
8
- }
9
- /**
10
- * Static utility class that detects overlapping HTTP routes and reports
11
- * them according to a per-kind policy. Stateless — every method takes
12
- * everything it needs as parameters.
13
- */
14
- export declare class RouteConflictDetector {
15
- /**
16
- * Strips the leading `:` / `*` marker (if present) and tags each
17
- * segment as a literal, named param, or named wildcard. Supports both
18
- * bare named wildcards (`*path`) and adapter-normalized path-to-regexp
19
- * wildcard groups (`{*path}`).
20
- */
21
- static tokenizePath(rawPath: string): PathSegment[];
22
- /**
23
- * Decides whether two paths can match the same incoming request, given
24
- * only their declared patterns (no host/method/version considered).
25
- */
26
- static pathsCanOverlap(leftPath: string, rightPath: string): boolean;
27
- /**
28
- * Walks every unique pair of resolved routes and produces a conflict
29
- * record for each pair whose (method, host, version, path) tuples can
30
- * collide at runtime.
31
- */
32
- static detect(routes: ResolvedRoute[], versioningOptions: VersioningOptions | undefined): RouteConflict[];
33
- /**
34
- * Applies the per-kind policy to a set of conflicts: silences `'off'`,
35
- * logs `'warn'` once per conflict, and aggregates every `'error'`-level
36
- * conflict into a single `RouteConflictException`.
37
- */
38
- static handle(conflicts: RouteConflict[], policy: RouteConflictPolicy | undefined, logger: Logger): void;
39
- /**
40
- * Removes shadow conflicts that specificity sorting has already resolved.
41
- *
42
- * When `routeResolutionStrategy: 'specificity'` is active, the sort
43
- * promotes more-specific routes ahead of less-specific ones. A shadow
44
- * where the sort promoted the winner (it was declared *later* but sorted
45
- * *first*) is handled correctly at runtime — the more-specific route is
46
- * registered first and handles its requests while the less-specific route
47
- * handles the rest. Retaining such a conflict would cause `shadow: 'error'`
48
- * to abort an application whose routes actually work as intended.
49
- *
50
- * Shadows where the winner was already first in declaration order (the
51
- * sort did not swap them) are genuine and are kept unchanged. Duplicate
52
- * conflicts are always kept.
53
- *
54
- * @param conflicts Conflicts detected on the sorted route list.
55
- * @param declarationOrder Routes in their original declaration order
56
- * (i.e. before specificity sorting was applied).
57
- */
58
- static filterSortResolvedShadows(conflicts: RouteConflict[], declarationOrder: ResolvedRoute[]): RouteConflict[];
59
- private static segmentsCanOverlap;
60
- private static methodsCanOverlap;
61
- private static versionsCanOverlap;
62
- private static hostsCanOverlap;
63
- private static hostValuesCanMatchSameRequest;
64
- private static routesAreIdentical;
65
- private static hostsAreIdentical;
66
- private static hostValuesAreIdentical;
67
- private static versionsAreIdentical;
68
- private static forEachUniquePair;
69
- private static describeConflict;
70
- }
71
- export {};
@@ -1,276 +0,0 @@
1
- import { RequestMethod, VERSION_NEUTRAL, VersioningType, } from '@nestjs/common';
2
- import { RouteConflictException } from '../errors/exceptions/route-conflict.exception.js';
3
- import { DUPLICATE_ROUTE_MESSAGE, SHADOWED_ROUTE_MESSAGE, } from '../errors/messages.js';
4
- /**
5
- * Static utility class that detects overlapping HTTP routes and reports
6
- * them according to a per-kind policy. Stateless — every method takes
7
- * everything it needs as parameters.
8
- */
9
- export class RouteConflictDetector {
10
- /**
11
- * Strips the leading `:` / `*` marker (if present) and tags each
12
- * segment as a literal, named param, or named wildcard. Supports both
13
- * bare named wildcards (`*path`) and adapter-normalized path-to-regexp
14
- * wildcard groups (`{*path}`).
15
- */
16
- static tokenizePath(rawPath) {
17
- const segments = [];
18
- rawPath
19
- .split('/')
20
- .filter(rawSegment => rawSegment.length > 0)
21
- .forEach(rawSegment => {
22
- if (rawSegment.startsWith('*')) {
23
- segments.push({ kind: 'wildcard', value: rawSegment.slice(1) });
24
- return;
25
- }
26
- if (rawSegment.startsWith('{*') && rawSegment.endsWith('}')) {
27
- segments.push({
28
- kind: 'wildcard',
29
- value: rawSegment.slice(2, -1),
30
- });
31
- return;
32
- }
33
- if (rawSegment.startsWith(':')) {
34
- segments.push({ kind: 'param', value: rawSegment.slice(1) });
35
- return;
36
- }
37
- segments.push({ kind: 'literal', value: rawSegment });
38
- });
39
- return segments;
40
- }
41
- /**
42
- * Decides whether two paths can match the same incoming request, given
43
- * only their declared patterns (no host/method/version considered).
44
- */
45
- static pathsCanOverlap(leftPath, rightPath) {
46
- const leftSegments = RouteConflictDetector.tokenizePath(leftPath);
47
- const rightSegments = RouteConflictDetector.tokenizePath(rightPath);
48
- const leftEndsInWildcard = leftSegments[leftSegments.length - 1]?.kind === 'wildcard';
49
- const rightEndsInWildcard = rightSegments[rightSegments.length - 1]?.kind === 'wildcard';
50
- // A named wildcard like `*path` requires at least one matched segment,
51
- // so only the *shorter* side's trailing wildcard can absorb the
52
- // difference. If the longer side has the wildcard, the other side
53
- // simply does not have enough segments to ever reach that position.
54
- if (leftSegments.length !== rightSegments.length) {
55
- const shorterEndsInWildcard = leftSegments.length < rightSegments.length
56
- ? leftEndsInWildcard
57
- : rightEndsInWildcard;
58
- if (!shorterEndsInWildcard) {
59
- return false;
60
- }
61
- }
62
- const sharedLength = Math.min(leftSegments.length, rightSegments.length);
63
- let canOverlap = true;
64
- leftSegments.slice(0, sharedLength).forEach((leftSegment, segmentIndex) => {
65
- if (!canOverlap)
66
- return;
67
- if (!RouteConflictDetector.segmentsCanOverlap(leftSegment, rightSegments[segmentIndex])) {
68
- canOverlap = false;
69
- }
70
- });
71
- return canOverlap;
72
- }
73
- /**
74
- * Walks every unique pair of resolved routes and produces a conflict
75
- * record for each pair whose (method, host, version, path) tuples can
76
- * collide at runtime.
77
- */
78
- static detect(routes, versioningOptions) {
79
- const conflicts = [];
80
- RouteConflictDetector.forEachUniquePair(routes, (earlierRoute, laterRoute) => {
81
- if (!RouteConflictDetector.methodsCanOverlap(earlierRoute.method, laterRoute.method)) {
82
- return;
83
- }
84
- if (!RouteConflictDetector.versionsCanOverlap(earlierRoute.version, laterRoute.version, versioningOptions)) {
85
- return;
86
- }
87
- if (!RouteConflictDetector.hostsCanOverlap(earlierRoute.host, laterRoute.host)) {
88
- return;
89
- }
90
- if (!RouteConflictDetector.pathsCanOverlap(earlierRoute.path, laterRoute.path)) {
91
- return;
92
- }
93
- const isIdentical = RouteConflictDetector.routesAreIdentical(earlierRoute, laterRoute, versioningOptions);
94
- conflicts.push({
95
- winner: earlierRoute,
96
- shadowed: laterRoute,
97
- kind: isIdentical ? 'duplicate' : 'shadow',
98
- });
99
- });
100
- return conflicts;
101
- }
102
- /**
103
- * Applies the per-kind policy to a set of conflicts: silences `'off'`,
104
- * logs `'warn'` once per conflict, and aggregates every `'error'`-level
105
- * conflict into a single `RouteConflictException`.
106
- */
107
- static handle(conflicts, policy, logger) {
108
- if (conflicts.length === 0 || policy === undefined)
109
- return;
110
- const errorMessages = [];
111
- conflicts.forEach(conflict => {
112
- const policyForKind = policy[conflict.kind] ?? 'off';
113
- if (policyForKind === 'off')
114
- return;
115
- const message = RouteConflictDetector.describeConflict(conflict);
116
- if (policyForKind === 'warn') {
117
- logger.warn(message);
118
- return;
119
- }
120
- errorMessages.push(message);
121
- });
122
- if (errorMessages.length > 0) {
123
- throw new RouteConflictException(errorMessages);
124
- }
125
- }
126
- /**
127
- * Removes shadow conflicts that specificity sorting has already resolved.
128
- *
129
- * When `routeResolutionStrategy: 'specificity'` is active, the sort
130
- * promotes more-specific routes ahead of less-specific ones. A shadow
131
- * where the sort promoted the winner (it was declared *later* but sorted
132
- * *first*) is handled correctly at runtime — the more-specific route is
133
- * registered first and handles its requests while the less-specific route
134
- * handles the rest. Retaining such a conflict would cause `shadow: 'error'`
135
- * to abort an application whose routes actually work as intended.
136
- *
137
- * Shadows where the winner was already first in declaration order (the
138
- * sort did not swap them) are genuine and are kept unchanged. Duplicate
139
- * conflicts are always kept.
140
- *
141
- * @param conflicts Conflicts detected on the sorted route list.
142
- * @param declarationOrder Routes in their original declaration order
143
- * (i.e. before specificity sorting was applied).
144
- */
145
- static filterSortResolvedShadows(conflicts, declarationOrder) {
146
- const declarationIndex = new Map(declarationOrder.map((route, idx) => [route, idx]));
147
- return conflicts.filter(conflict => {
148
- if (conflict.kind !== 'shadow')
149
- return true;
150
- const winnerDeclIdx = declarationIndex.get(conflict.winner) ?? -1;
151
- const shadowedDeclIdx = declarationIndex.get(conflict.shadowed) ?? -1;
152
- // The sort promoted the winner (declared later, but sorted to the
153
- // front because it is more specific). The shadow is resolved at
154
- // runtime — drop it. Keep only genuine shadows where the winner was
155
- // already first in declaration order.
156
- return winnerDeclIdx < shadowedDeclIdx;
157
- });
158
- }
159
- static segmentsCanOverlap(leftSegment, rightSegment) {
160
- if (leftSegment.kind === 'wildcard' || rightSegment.kind === 'wildcard') {
161
- return true;
162
- }
163
- if (leftSegment.kind === 'param' || rightSegment.kind === 'param') {
164
- return true;
165
- }
166
- return leftSegment.value === rightSegment.value;
167
- }
168
- static methodsCanOverlap(leftMethod, rightMethod) {
169
- if (leftMethod === RequestMethod.ALL || rightMethod === RequestMethod.ALL) {
170
- return true;
171
- }
172
- return leftMethod === rightMethod;
173
- }
174
- static versionsCanOverlap(leftVersion, rightVersion, versioningOptions) {
175
- if (!versioningOptions)
176
- return true;
177
- if (versioningOptions.type === VersioningType.URI)
178
- return true;
179
- const leftMatchesAnyVersion = leftVersion === undefined || leftVersion === VERSION_NEUTRAL;
180
- const rightMatchesAnyVersion = rightVersion === undefined || rightVersion === VERSION_NEUTRAL;
181
- if (leftMatchesAnyVersion || rightMatchesAnyVersion)
182
- return true;
183
- const leftValues = Array.isArray(leftVersion) ? leftVersion : [leftVersion];
184
- const rightValues = Array.isArray(rightVersion)
185
- ? rightVersion
186
- : [rightVersion];
187
- return leftValues.some(versionValue => rightValues.includes(versionValue));
188
- }
189
- static hostsCanOverlap(leftHost, rightHost) {
190
- if (leftHost === undefined || rightHost === undefined)
191
- return true;
192
- const leftHosts = Array.isArray(leftHost) ? leftHost : [leftHost];
193
- const rightHosts = Array.isArray(rightHost) ? rightHost : [rightHost];
194
- return leftHosts.some(leftValue => rightHosts.some(rightValue => RouteConflictDetector.hostValuesCanMatchSameRequest(leftValue, rightValue)));
195
- }
196
- static hostValuesCanMatchSameRequest(leftValue, rightValue) {
197
- const leftIsRegExp = leftValue instanceof RegExp;
198
- const rightIsRegExp = rightValue instanceof RegExp;
199
- if (leftIsRegExp && rightIsRegExp)
200
- return true;
201
- // Reset lastIndex before calling test() to guard against RegExps with the
202
- // `g` or `y` flags: those are stateful and would produce inconsistent
203
- // results (false negatives) when the same instance is reused across the
204
- // multiple pair comparisons that a single detect() run performs.
205
- if (leftIsRegExp) {
206
- leftValue.lastIndex = 0;
207
- return leftValue.test(rightValue);
208
- }
209
- if (rightIsRegExp) {
210
- rightValue.lastIndex = 0;
211
- return rightValue.test(leftValue);
212
- }
213
- return leftValue === rightValue;
214
- }
215
- static routesAreIdentical(leftRoute, rightRoute, versioningOptions) {
216
- return (leftRoute.method === rightRoute.method &&
217
- leftRoute.path === rightRoute.path &&
218
- RouteConflictDetector.hostsAreIdentical(leftRoute.host, rightRoute.host) &&
219
- RouteConflictDetector.versionsAreIdentical(leftRoute.version, rightRoute.version, versioningOptions));
220
- }
221
- static hostsAreIdentical(leftHost, rightHost) {
222
- if (leftHost === undefined && rightHost === undefined)
223
- return true;
224
- if (leftHost === undefined || rightHost === undefined)
225
- return false;
226
- const leftHosts = Array.isArray(leftHost) ? leftHost : [leftHost];
227
- const rightHosts = Array.isArray(rightHost) ? rightHost : [rightHost];
228
- if (leftHosts.length !== rightHosts.length)
229
- return false;
230
- // Order-insensitive set comparison: ['a', 'b'] and ['b', 'a']
231
- // describe the same allowed-host set, so they are identical for
232
- // duplicate-classification purposes.
233
- return leftHosts.every(leftValue => rightHosts.some(rightValue => RouteConflictDetector.hostValuesAreIdentical(leftValue, rightValue)));
234
- }
235
- static hostValuesAreIdentical(leftValue, rightValue) {
236
- if (leftValue instanceof RegExp && rightValue instanceof RegExp) {
237
- return (leftValue.source === rightValue.source &&
238
- leftValue.flags === rightValue.flags);
239
- }
240
- return leftValue === rightValue;
241
- }
242
- static versionsAreIdentical(leftVersion, rightVersion, versioningOptions) {
243
- // When versioning is not configured (or URI-based, where the
244
- // version is encoded in the path), version metadata does not
245
- // gate request matching at runtime, so two routes that differ
246
- // only in their declared `version` are runtime duplicates.
247
- if (!versioningOptions || versioningOptions.type === VersioningType.URI) {
248
- return true;
249
- }
250
- if (leftVersion === rightVersion)
251
- return true;
252
- const leftValues = Array.isArray(leftVersion) ? leftVersion : [leftVersion];
253
- const rightValues = Array.isArray(rightVersion)
254
- ? rightVersion
255
- : [rightVersion];
256
- if (leftValues.length !== rightValues.length)
257
- return false;
258
- return leftValues.every(value => rightValues.includes(value));
259
- }
260
- static forEachUniquePair(items, visit) {
261
- items.forEach((leftItem, leftIndex) => {
262
- items.slice(leftIndex + 1).forEach(rightItem => {
263
- visit(leftItem, rightItem);
264
- });
265
- });
266
- }
267
- static describeConflict(conflict) {
268
- const method = RequestMethod[conflict.winner.method];
269
- const winnerLabel = `${conflict.winner.instanceWrapper.name}#${conflict.winner.methodName}`;
270
- const shadowedLabel = `${conflict.shadowed.instanceWrapper.name}#${conflict.shadowed.methodName}`;
271
- if (conflict.kind === 'duplicate') {
272
- return DUPLICATE_ROUTE_MESSAGE(method, conflict.winner.path, winnerLabel, shadowedLabel);
273
- }
274
- return SHADOWED_ROUTE_MESSAGE(method, conflict.shadowed.path, shadowedLabel, conflict.winner.path, winnerLabel);
275
- }
276
- }
@@ -1,23 +0,0 @@
1
- import { ResolvedRoute } from './interfaces/resolved-route.interface.js';
2
- /**
3
- * Static utility class that orders resolved routes by specificity so the
4
- * underlying HTTP adapter registers more specific patterns first.
5
- * Stateless — every method takes everything it needs as parameters.
6
- */
7
- export declare class RouteSpecificitySorter {
8
- /**
9
- * Lower rank means more specific. A literal segment beats a named
10
- * param, which beats a named wildcard. A position that is absent on
11
- * one side is the least specific of all (it means the path is shorter
12
- * at that point).
13
- */
14
- private static readonly SEGMENT_KIND_RANK;
15
- /**
16
- * Returns a new array of routes sorted from most-specific to
17
- * least-specific. Routes that tie on specificity keep their original
18
- * declaration order.
19
- */
20
- static sort(routes: ResolvedRoute[]): ResolvedRoute[];
21
- private static comparePathSpecificity;
22
- private static rankSegmentByKind;
23
- }
@@ -1,59 +0,0 @@
1
- import { RouteConflictDetector } from './route-conflict-detector.js';
2
- /**
3
- * Static utility class that orders resolved routes by specificity so the
4
- * underlying HTTP adapter registers more specific patterns first.
5
- * Stateless — every method takes everything it needs as parameters.
6
- */
7
- export class RouteSpecificitySorter {
8
- /**
9
- * Lower rank means more specific. A literal segment beats a named
10
- * param, which beats a named wildcard. A position that is absent on
11
- * one side is the least specific of all (it means the path is shorter
12
- * at that point).
13
- */
14
- static SEGMENT_KIND_RANK = {
15
- literal: 0,
16
- param: 1,
17
- wildcard: 2,
18
- missing: 3,
19
- };
20
- /**
21
- * Returns a new array of routes sorted from most-specific to
22
- * least-specific. Routes that tie on specificity keep their original
23
- * declaration order.
24
- */
25
- static sort(routes) {
26
- const decoratedRoutes = routes.map((route, declarationIndex) => ({
27
- route,
28
- declarationIndex,
29
- }));
30
- decoratedRoutes.sort((leftEntry, rightEntry) => {
31
- const specificityDelta = RouteSpecificitySorter.comparePathSpecificity(leftEntry.route.path, rightEntry.route.path);
32
- if (specificityDelta !== 0)
33
- return specificityDelta;
34
- return leftEntry.declarationIndex - rightEntry.declarationIndex;
35
- });
36
- return decoratedRoutes.map(decoratedEntry => decoratedEntry.route);
37
- }
38
- static comparePathSpecificity(leftPath, rightPath) {
39
- const leftSegments = RouteConflictDetector.tokenizePath(leftPath);
40
- const rightSegments = RouteConflictDetector.tokenizePath(rightPath);
41
- const longestPathLength = Math.max(leftSegments.length, rightSegments.length);
42
- let specificityDelta = 0;
43
- Array.from({ length: longestPathLength }).forEach((_, segmentIndex) => {
44
- if (specificityDelta !== 0)
45
- return;
46
- const leftKind = leftSegments[segmentIndex]?.kind ?? 'missing';
47
- const rightKind = rightSegments[segmentIndex]?.kind ?? 'missing';
48
- const leftRank = RouteSpecificitySorter.rankSegmentByKind(leftKind);
49
- const rightRank = RouteSpecificitySorter.rankSegmentByKind(rightKind);
50
- if (leftRank !== rightRank) {
51
- specificityDelta = leftRank - rightRank;
52
- }
53
- });
54
- return specificityDelta;
55
- }
56
- static rankSegmentByKind(kind) {
57
- return RouteSpecificitySorter.SEGMENT_KIND_RANK[kind];
58
- }
59
- }