@real-router/route-utils 0.1.5 → 0.1.7

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 +1 @@
1
- {"version":3,"sources":["../../src/routeRelation.ts","../../src/constants.ts","../../src/segmentTesters.ts","../../src/RouteUtils.ts","../../src/getRouteUtils.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// packages/route-utils/src/routeRelation.ts\n\n/**\n * Checks if two routes are related in the hierarchy.\n *\n * Routes are related if:\n * - They are exactly the same\n * - One is a parent of the other (e.g., \"users\" and \"users.list\")\n * - One is a child of the other (e.g., \"users.list\" and \"users\")\n *\n * @param route1 - First route name\n * @param route2 - Second route name\n * @returns True if routes are related, false otherwise\n *\n * @example\n * areRoutesRelated(\"users\", \"users.list\"); // true (parent-child)\n * areRoutesRelated(\"users.list\", \"users\"); // true (child-parent)\n * areRoutesRelated(\"users\", \"users\"); // true (same)\n * areRoutesRelated(\"users\", \"admin\"); // false (different branches)\n * areRoutesRelated(\"users.list\", \"users.view\"); // false (siblings)\n */\nexport function areRoutesRelated(route1: string, route2: string): boolean {\n return (\n route1 === route2 ||\n route1.startsWith(`${route2}.`) ||\n route2.startsWith(`${route1}.`)\n );\n}\n","// packages/route-utils/src/constants.ts\n\n/**\n * Maximum allowed segment length (10,000 characters)\n */\nexport const MAX_SEGMENT_LENGTH = 10_000;\n\n/**\n * Pattern for valid segment characters: alphanumeric + dot + dash + underscore\n * Uses explicit character ranges for clarity and portability.\n * Dash is placed at the end to avoid escaping (no range operator confusion).\n */\nexport const SAFE_SEGMENT_PATTERN = /^[\\w.-]+$/;\n\n/**\n * Route segment separator character\n */\nexport const ROUTE_SEGMENT_SEPARATOR = \".\";\n","// packages/route-utils/src/segmentTesters.ts\n\nimport {\n MAX_SEGMENT_LENGTH,\n ROUTE_SEGMENT_SEPARATOR,\n SAFE_SEGMENT_PATTERN,\n} from \"./constants\";\n\nimport type { SegmentTestFunction } from \"./types\";\nimport type { State } from \"@real-router/types\";\n\n/**\n * Escapes special RegExp characters in a string.\n * Handles all RegExp metacharacters including dash in character classes.\n *\n * @param str - String to escape\n * @returns Escaped string safe for RegExp construction\n * @internal\n */\nconst escapeRegExp = (str: string): string =>\n str.replaceAll(/[$()*+.?[\\\\\\]^{|}-]/g, String.raw`\\$&`);\n\n/**\n * Creates a segment tester function with specified start and end patterns.\n * This is a factory function that produces the actual test functions.\n *\n * @param start - RegExp pattern for start (e.g., \"^\" for startsWith)\n * @param end - RegExp pattern for end (e.g., \"$\" or dotOrEnd for specific matching)\n * @returns A test function that can check if routes match the segment pattern\n * @internal\n */\nconst makeSegmentTester = (start: string, end: string) => {\n const regexCache = new Map<string, RegExp>();\n\n /**\n * Builds a RegExp for testing segment matches.\n * Validates length and character pattern. Type and empty checks are done by caller.\n *\n * This optimizes performance by avoiding redundant checks - callers verify\n * type and empty before calling this function.\n *\n * @param segment - The segment to build a regex for (non-empty string, pre-validated)\n * @returns RegExp for testing\n * @throws {RangeError} If segment exceeds maximum length\n * @throws {TypeError} If segment contains invalid characters\n * @internal\n */\n const buildRegex = (segment: string): RegExp => {\n const cached = regexCache.get(segment);\n\n if (cached) {\n return cached;\n }\n\n // Type and empty checks are SKIPPED - caller already verified these\n\n // Length check\n if (segment.length > MAX_SEGMENT_LENGTH) {\n throw new RangeError(\n `Segment exceeds maximum length of ${MAX_SEGMENT_LENGTH} characters`,\n );\n }\n\n // Character pattern check\n if (!SAFE_SEGMENT_PATTERN.test(segment)) {\n throw new TypeError(\n `Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)`,\n );\n }\n\n const regex = new RegExp(start + escapeRegExp(segment) + end);\n\n regexCache.set(segment, regex);\n\n return regex;\n };\n\n // TypeScript cannot infer conditional return type for curried function with union return.\n // The function returns either boolean or a tester function based on whether segment is provided.\n // This is an intentional design pattern for API flexibility.\n // eslint-disable-next-line sonarjs/function-return-type\n return (route: State | string, segment?: string | null) => {\n // Extract route name, handling both string and State object inputs\n // State.name is always string by real-router type definition\n const name = typeof route === \"string\" ? route : route.name;\n\n if (typeof name !== \"string\") {\n return false;\n }\n\n // Empty route name always returns false\n if (name.length === 0) {\n return false;\n }\n\n // null always returns false (consistent behavior)\n if (segment === null) {\n return false;\n }\n\n // Currying: if no segment provided, return a tester function\n if (segment === undefined) {\n return (localSegment: string) => {\n // Type check for runtime safety (consistent with direct call)\n if (typeof localSegment !== \"string\") {\n throw new TypeError(\n `Segment must be a string, got ${typeof localSegment}`,\n );\n }\n\n // Empty string returns false (consistent with direct call)\n if (localSegment.length === 0) {\n return false;\n }\n\n // Use buildRegex (type and empty checks already done above)\n return buildRegex(localSegment).test(name);\n };\n }\n\n if (typeof segment !== \"string\") {\n // Runtime protection: TypeScript already narrows to 'string' here,\n // but we keep this check for defense against unexpected runtime values\n throw new TypeError(`Segment must be a string, got ${typeof segment}`);\n }\n\n // Empty string returns false (consistent behavior)\n if (segment.length === 0) {\n return false;\n }\n\n // Perform the actual regex test\n // buildRegex skips type and empty checks (already validated above)\n return buildRegex(segment).test(name);\n };\n};\n\n/**\n * Pattern that matches either a dot separator or end of string.\n * Used for prefix/suffix matching that respects segment boundaries.\n */\nconst dotOrEnd = `(?:${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)}|$)`;\n\n/**\n * Tests if a route name starts with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route starts with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * startsWithSegment('users.list', 'users'); // true\n * startsWithSegment('users.list', 'admin'); // false\n *\n * @example\n * // Curried form\n * const tester = startsWithSegment('users.list');\n * tester('users'); // true\n * tester('admin'); // false\n *\n * @example\n * // With State object\n * const state: State = { name: 'users.list', params: {}, path: '/users/list' };\n * startsWithSegment(state, 'users'); // true\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see endsWithSegment for suffix matching\n * @see includesSegment for anywhere matching\n */\nexport const startsWithSegment = makeSegmentTester(\n \"^\",\n dotOrEnd,\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name ends with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route ends with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * endsWithSegment('users.list', 'list'); // true\n * endsWithSegment('users.profile.edit', 'edit'); // true\n *\n * @example\n * // Curried form\n * const tester = endsWithSegment('users.list');\n * tester('list'); // true\n * tester('users'); // false\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see includesSegment for anywhere matching\n */\nexport const endsWithSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n \"$\",\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name includes the given segment anywhere in its path.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route includes segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * includesSegment('users.profile.edit', 'profile'); // true\n *\n * @example\n * // Multi-segment inclusion\n * includesSegment('a.b.c.d', 'b.c'); // true\n * includesSegment('a.b.c.d', 'a.c'); // false (must be contiguous)\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see endsWithSegment for suffix matching\n */\nexport const includesSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n dotOrEnd,\n) as SegmentTestFunction;\n","import { areRoutesRelated } from \"./routeRelation.js\";\nimport {\n startsWithSegment,\n endsWithSegment,\n includesSegment,\n} from \"./segmentTesters.js\";\n\nimport type { RouteTreeNode, SegmentTestFunction } from \"./types.js\";\n\nexport class RouteUtils {\n // ===== Static facade: segment testing =====\n\n /**\n * Tests if a route name starts with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link startsWithSegment} standalone function for details\n */\n static readonly startsWithSegment: SegmentTestFunction = startsWithSegment;\n\n /**\n * Tests if a route name ends with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link endsWithSegment} standalone function for details\n */\n static readonly endsWithSegment: SegmentTestFunction = endsWithSegment;\n\n /**\n * Tests if a route name includes the given segment anywhere in its path.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link includesSegment} standalone function for details\n */\n static readonly includesSegment: SegmentTestFunction = includesSegment;\n\n /**\n * Checks if two routes are related in the hierarchy\n * (same, parent-child, or child-parent).\n *\n * @see {@link areRoutesRelated} standalone function for details\n */\n static readonly areRoutesRelated = areRoutesRelated;\n\n // ===== Instance fields =====\n\n readonly #chainCache: Map<string, readonly string[]>;\n readonly #siblingsCache: Map<string, readonly string[]>;\n\n constructor(root: RouteTreeNode) {\n this.#chainCache = new Map();\n this.#siblingsCache = new Map();\n this.#buildAll(root, []);\n }\n\n /**\n * Returns cumulative name segments for the given route (ancestor chain without root).\n *\n * All chains are pre-computed and frozen during construction.\n *\n * @param name - Full route name (e.g. `\"users.profile\"`)\n * @returns Frozen array of cumulative segments, or `undefined` if not in tree\n *\n * @example\n * ```ts\n * utils.getChain(\"users.profile.edit\");\n * // → [\"users\", \"users.profile\", \"users.profile.edit\"]\n * ```\n */\n getChain(name: string): readonly string[] | undefined {\n return this.#chainCache.get(name);\n }\n\n /**\n * Returns non-absolute siblings of the named node (excluding itself).\n *\n * Siblings are children of the same parent, filtered by `nonAbsoluteChildren`.\n * All siblings are pre-computed and frozen during construction.\n *\n * @param name - Full route name\n * @returns Frozen array of sibling full names, or `undefined` if not found or root\n */\n getSiblings(name: string): readonly string[] | undefined {\n return this.#siblingsCache.get(name);\n }\n\n /**\n * Checks if `child` is a descendant of `parent` via string prefix comparison.\n *\n * Does not perform tree lookup — O(k) where k is the name length.\n *\n * @param child - Full name of the potential descendant\n * @param parent - Full name of the potential ancestor\n * @returns `true` if `child` starts with `parent.` (dot-separated)\n *\n * @remarks\n * Does not work with root (`\"\"`) as parent — returns `false` because\n * `\"users\".startsWith(\".\")` is `false`. This is acceptable since\n * every route in the tree is trivially a descendant of root.\n */\n isDescendantOf(child: string, parent: string): boolean {\n return child !== parent && child.startsWith(`${parent}.`);\n }\n #buildAll(node: RouteTreeNode, chain: string[]): void {\n const { fullName } = node;\n\n // Build chain: root gets [\"\"], others get cumulative segments\n if (fullName !== \"\") {\n chain.push(fullName);\n }\n\n this.#chainCache.set(\n fullName,\n Object.freeze(fullName === \"\" ? [\"\"] : [...chain]),\n );\n\n // Build siblings for all children of this node\n // Siblings = nonAbsoluteChildren excluding the child itself\n // Absolute children also get siblings (all nonAbsoluteChildren)\n const nonAbsoluteNames = node.nonAbsoluteChildren.map((c) => c.fullName);\n\n for (const child of node.nonAbsoluteChildren) {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze(nonAbsoluteNames.filter((n) => n !== child.fullName)),\n );\n }\n\n // Absolute children: their siblings are ALL nonAbsoluteChildren\n for (const child of node.children.values()) {\n if (!this.#siblingsCache.has(child.fullName) && child.fullName !== \"\") {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze([...nonAbsoluteNames]),\n );\n }\n }\n\n // Recurse into all children (including absolute)\n for (const child of node.children.values()) {\n this.#buildAll(child, chain);\n }\n\n // Restore chain for sibling traversal\n if (fullName !== \"\") {\n chain.pop();\n }\n }\n}\n","import { RouteUtils } from \"./RouteUtils.js\";\n\nimport type { RouteTreeNode } from \"./types.js\";\n\nconst cache = new WeakMap<RouteTreeNode, RouteUtils>();\n\nexport function getRouteUtils(root: RouteTreeNode): RouteUtils {\n let utils = cache.get(root);\n\n if (utils === undefined) {\n utils = new RouteUtils(root);\n cache.set(root, utils);\n }\n\n return utils;\n}\n"]}
1
+ {"version":3,"sources":["../../src/routeRelation.ts","../../src/constants.ts","../../src/segmentTesters.ts","../../src/RouteUtils.ts","../../src/getRouteUtils.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// packages/route-utils/src/routeRelation.ts\n\n/**\n * Checks if two routes are related in the hierarchy.\n *\n * Routes are related if:\n * - They are exactly the same\n * - One is a parent of the other (e.g., \"users\" and \"users.list\")\n * - One is a child of the other (e.g., \"users.list\" and \"users\")\n *\n * @param route1 - First route name\n * @param route2 - Second route name\n * @returns True if routes are related, false otherwise\n *\n * @example\n * areRoutesRelated(\"users\", \"users.list\"); // true (parent-child)\n * areRoutesRelated(\"users.list\", \"users\"); // true (child-parent)\n * areRoutesRelated(\"users\", \"users\"); // true (same)\n * areRoutesRelated(\"users\", \"admin\"); // false (different branches)\n * areRoutesRelated(\"users.list\", \"users.view\"); // false (siblings)\n */\nexport function areRoutesRelated(route1: string, route2: string): boolean {\n return (\n route1 === route2 ||\n route1.startsWith(`${route2}.`) ||\n route2.startsWith(`${route1}.`)\n );\n}\n","// packages/route-utils/src/constants.ts\n\n/**\n * Maximum allowed segment length (10,000 characters)\n */\nexport const MAX_SEGMENT_LENGTH = 10_000;\n\n/**\n * Pattern for valid segment characters: alphanumeric + dot + dash + underscore\n * Uses explicit character ranges for clarity and portability.\n * Dash is placed at the end to avoid escaping (no range operator confusion).\n */\nexport const SAFE_SEGMENT_PATTERN = /^[\\w.-]+$/;\n\n/**\n * Route segment separator character\n */\nexport const ROUTE_SEGMENT_SEPARATOR = \".\";\n","// packages/route-utils/src/segmentTesters.ts\n\nimport {\n MAX_SEGMENT_LENGTH,\n ROUTE_SEGMENT_SEPARATOR,\n SAFE_SEGMENT_PATTERN,\n} from \"./constants\";\n\nimport type { SegmentTestFunction } from \"./types\";\nimport type { State } from \"@real-router/types\";\n\n/**\n * Escapes special RegExp characters in a string.\n * Handles all RegExp metacharacters including dash in character classes.\n *\n * @param str - String to escape\n * @returns Escaped string safe for RegExp construction\n * @internal\n */\nconst escapeRegExp = (str: string): string =>\n str.replaceAll(/[$()*+.?[\\\\\\]^{|}-]/g, String.raw`\\$&`);\n\n/**\n * Creates a segment tester function with specified start and end patterns.\n * This is a factory function that produces the actual test functions.\n *\n * @param start - RegExp pattern for start (e.g., \"^\" for startsWith)\n * @param end - RegExp pattern for end (e.g., \"$\" or dotOrEnd for specific matching)\n * @returns A test function that can check if routes match the segment pattern\n * @internal\n */\nconst makeSegmentTester = (start: string, end: string) => {\n const regexCache = new Map<string, RegExp>();\n\n /**\n * Builds a RegExp for testing segment matches.\n * Validates length and character pattern. Type and empty checks are done by caller.\n *\n * This optimizes performance by avoiding redundant checks - callers verify\n * type and empty before calling this function.\n *\n * @param segment - The segment to build a regex for (non-empty string, pre-validated)\n * @returns RegExp for testing\n * @throws {RangeError} If segment exceeds maximum length\n * @throws {TypeError} If segment contains invalid characters\n * @internal\n */\n const buildRegex = (segment: string): RegExp => {\n const cached = regexCache.get(segment);\n\n if (cached) {\n return cached;\n }\n\n // Type and empty checks are SKIPPED - caller already verified these\n\n // Length check\n if (segment.length > MAX_SEGMENT_LENGTH) {\n throw new RangeError(\n `Segment exceeds maximum length of ${MAX_SEGMENT_LENGTH} characters`,\n );\n }\n\n // Character pattern check\n if (!SAFE_SEGMENT_PATTERN.test(segment)) {\n throw new TypeError(\n `Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)`,\n );\n }\n\n const regex = new RegExp(start + escapeRegExp(segment) + end);\n\n regexCache.set(segment, regex);\n\n return regex;\n };\n\n // TypeScript cannot infer conditional return type for curried function with union return.\n // The function returns either boolean or a tester function based on whether segment is provided.\n // This is an intentional design pattern for API flexibility.\n // eslint-disable-next-line sonarjs/function-return-type\n return (route: State | string, segment?: string | null) => {\n // Extract route name, handling both string and State object inputs\n // State.name is always string by real-router type definition\n const name = typeof route === \"string\" ? route : route.name;\n\n if (typeof name !== \"string\") {\n return false;\n }\n\n // Empty route name always returns false\n if (name.length === 0) {\n return false;\n }\n\n // null always returns false (consistent behavior)\n if (segment === null) {\n return false;\n }\n\n // Currying: if no segment provided, return a tester function\n if (segment === undefined) {\n return (localSegment: string) => {\n // Type check for runtime safety (consistent with direct call)\n if (typeof localSegment !== \"string\") {\n throw new TypeError(\n `Segment must be a string, got ${typeof localSegment}`,\n );\n }\n\n // Empty string returns false (consistent with direct call)\n if (localSegment.length === 0) {\n return false;\n }\n\n // Use buildRegex (type and empty checks already done above)\n return buildRegex(localSegment).test(name);\n };\n }\n\n if (typeof segment !== \"string\") {\n // Runtime protection: TypeScript already narrows to 'string' here,\n // but we keep this check for defense against unexpected runtime values\n throw new TypeError(`Segment must be a string, got ${typeof segment}`);\n }\n\n // Empty string returns false (consistent behavior)\n if (segment.length === 0) {\n return false;\n }\n\n // Perform the actual regex test\n // buildRegex skips type and empty checks (already validated above)\n return buildRegex(segment).test(name);\n };\n};\n\n/**\n * Pattern that matches either a dot separator or end of string.\n * Used for prefix/suffix matching that respects segment boundaries.\n */\nconst dotOrEnd = `(?:${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)}|$)`;\n\n/**\n * Tests if a route name starts with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route starts with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * startsWithSegment('users.list', 'users'); // true\n * startsWithSegment('users.list', 'admin'); // false\n *\n * @example\n * // Curried form\n * const tester = startsWithSegment('users.list');\n * tester('users'); // true\n * tester('admin'); // false\n *\n * @example\n * // With State object\n * const state: State = { name: 'users.list', params: {}, path: '/users/list' };\n * startsWithSegment(state, 'users'); // true\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see endsWithSegment for suffix matching\n * @see includesSegment for anywhere matching\n */\nexport const startsWithSegment = makeSegmentTester(\n \"^\",\n dotOrEnd,\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name ends with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route ends with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * endsWithSegment('users.list', 'list'); // true\n * endsWithSegment('users.profile.edit', 'edit'); // true\n *\n * @example\n * // Curried form\n * const tester = endsWithSegment('users.list');\n * tester('list'); // true\n * tester('users'); // false\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see includesSegment for anywhere matching\n */\nexport const endsWithSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n \"$\",\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name includes the given segment anywhere in its path.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route includes segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * includesSegment('users.profile.edit', 'profile'); // true\n *\n * @example\n * // Multi-segment inclusion\n * includesSegment('a.b.c.d', 'b.c'); // true\n * includesSegment('a.b.c.d', 'a.c'); // false (must be contiguous)\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see endsWithSegment for suffix matching\n */\nexport const includesSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n dotOrEnd,\n) as SegmentTestFunction;\n","import { areRoutesRelated } from \"./routeRelation.js\";\nimport {\n startsWithSegment,\n endsWithSegment,\n includesSegment,\n} from \"./segmentTesters.js\";\n\nimport type { RouteTreeNode, SegmentTestFunction } from \"./types.js\";\n\nexport class RouteUtils {\n // ===== Static facade: segment testing =====\n\n /**\n * Tests if a route name starts with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link startsWithSegment} standalone function for details\n */\n static readonly startsWithSegment: SegmentTestFunction = startsWithSegment;\n\n /**\n * Tests if a route name ends with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link endsWithSegment} standalone function for details\n */\n static readonly endsWithSegment: SegmentTestFunction = endsWithSegment;\n\n /**\n * Tests if a route name includes the given segment anywhere in its path.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link includesSegment} standalone function for details\n */\n static readonly includesSegment: SegmentTestFunction = includesSegment;\n\n /**\n * Checks if two routes are related in the hierarchy\n * (same, parent-child, or child-parent).\n *\n * @see {@link areRoutesRelated} standalone function for details\n */\n static readonly areRoutesRelated = areRoutesRelated;\n\n // ===== Instance fields =====\n\n readonly #chainCache: Map<string, readonly string[]>;\n readonly #siblingsCache: Map<string, readonly string[]>;\n\n constructor(root: RouteTreeNode) {\n this.#chainCache = new Map();\n this.#siblingsCache = new Map();\n this.#buildAll(root, []);\n }\n\n /**\n * Returns cumulative name segments for the given route (ancestor chain without root).\n *\n * All chains are pre-computed and frozen during construction.\n *\n * @param name - Full route name (e.g. `\"users.profile\"`)\n * @returns Frozen array of cumulative segments, or `undefined` if not in tree\n *\n * @example\n * ```ts\n * utils.getChain(\"users.profile.edit\");\n * // → [\"users\", \"users.profile\", \"users.profile.edit\"]\n * ```\n */\n getChain(name: string): readonly string[] | undefined {\n return this.#chainCache.get(name);\n }\n\n /**\n * Returns non-absolute siblings of the named node (excluding itself).\n *\n * Siblings are children of the same parent, filtered by `nonAbsoluteChildren`.\n * All siblings are pre-computed and frozen during construction.\n *\n * @param name - Full route name\n * @returns Frozen array of sibling full names, or `undefined` if not found or root\n */\n getSiblings(name: string): readonly string[] | undefined {\n return this.#siblingsCache.get(name);\n }\n\n /**\n * Checks if `child` is a descendant of `parent` via string prefix comparison.\n *\n * Does not perform tree lookup — O(k) where k is the name length.\n *\n * @param child - Full name of the potential descendant\n * @param parent - Full name of the potential ancestor\n * @returns `true` if `child` starts with `parent.` (dot-separated)\n *\n * @remarks\n * Does not work with root (`\"\"`) as parent — returns `false` because\n * `\"users\".startsWith(\".\")` is `false`. This is acceptable since\n * every route in the tree is trivially a descendant of root.\n */\n isDescendantOf(child: string, parent: string): boolean {\n return child !== parent && child.startsWith(`${parent}.`);\n }\n #buildAll(node: RouteTreeNode, chain: string[]): void {\n const { fullName } = node;\n\n // Build chain: root gets [\"\"], others get cumulative segments\n if (fullName !== \"\") {\n chain.push(fullName);\n }\n\n this.#chainCache.set(\n fullName,\n Object.freeze(fullName === \"\" ? [\"\"] : [...chain]),\n );\n\n // Build siblings for all children of this node\n // Siblings = nonAbsoluteChildren excluding the child itself\n // Absolute children also get siblings (all nonAbsoluteChildren)\n const nonAbsoluteNames = node.nonAbsoluteChildren.map(\n (child) => child.fullName,\n );\n\n for (const child of node.nonAbsoluteChildren) {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze(\n nonAbsoluteNames.filter((name) => name !== child.fullName),\n ),\n );\n }\n\n // Absolute children: their siblings are ALL nonAbsoluteChildren\n for (const child of node.children.values()) {\n if (!this.#siblingsCache.has(child.fullName) && child.fullName !== \"\") {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze([...nonAbsoluteNames]),\n );\n }\n }\n\n // Recurse into all children (including absolute)\n for (const child of node.children.values()) {\n this.#buildAll(child, chain);\n }\n\n // Restore chain for sibling traversal\n if (fullName !== \"\") {\n chain.pop();\n }\n }\n}\n","import { RouteUtils } from \"./RouteUtils.js\";\n\nimport type { RouteTreeNode } from \"./types.js\";\n\nconst cache = new WeakMap<RouteTreeNode, RouteUtils>();\n\nexport function getRouteUtils(root: RouteTreeNode): RouteUtils {\n let utils = cache.get(root);\n\n if (utils === undefined) {\n utils = new RouteUtils(root);\n cache.set(root, utils);\n }\n\n return utils;\n}\n"]}
@@ -1 +1 @@
1
- {"inputs":{"../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js":{"bytes":569,"imports":[],"format":"esm"},"src/routeRelation.ts":{"bytes":994,"imports":[{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/constants.ts":{"bytes":515,"imports":[{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/segmentTesters.ts":{"bytes":8570,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/RouteUtils.ts":{"bytes":4807,"imports":[{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/getRouteUtils.ts":{"bytes":365,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/index.ts":{"bytes":310,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"},{"path":"src/getRouteUtils.ts","kind":"import-statement","original":"./getRouteUtils.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"},{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"}},"outputs":{"dist/cjs/index.js.map":{"imports":[],"exports":[],"inputs":{},"bytes":19261},"dist/cjs/index.js":{"imports":[],"exports":["RouteUtils","areRoutesRelated","endsWithSegment","getRouteUtils","includesSegment","startsWithSegment"],"entryPoint":"src/index.ts","inputs":{"src/routeRelation.ts":{"bytesInOutput":144},"src/constants.ts":{"bytesInOutput":105},"src/segmentTesters.ts":{"bytesInOutput":1998},"src/RouteUtils.ts":{"bytesInOutput":3845},"src/index.ts":{"bytesInOutput":0},"src/getRouteUtils.ts":{"bytesInOutput":215}},"bytes":6546}}}
1
+ {"inputs":{"../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@6.0.2_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js":{"bytes":569,"imports":[],"format":"esm"},"src/routeRelation.ts":{"bytes":994,"imports":[{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@6.0.2_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/constants.ts":{"bytes":515,"imports":[{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@6.0.2_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/segmentTesters.ts":{"bytes":8570,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@6.0.2_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/RouteUtils.ts":{"bytes":4855,"imports":[{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@6.0.2_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/getRouteUtils.ts":{"bytes":365,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@6.0.2_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/index.ts":{"bytes":310,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"},{"path":"src/getRouteUtils.ts","kind":"import-statement","original":"./getRouteUtils.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"},{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@6.0.2_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"}},"outputs":{"dist/cjs/index.js.map":{"imports":[],"exports":[],"inputs":{},"bytes":19334},"dist/cjs/index.js":{"imports":[],"exports":["RouteUtils","areRoutesRelated","endsWithSegment","getRouteUtils","includesSegment","startsWithSegment"],"entryPoint":"src/index.ts","inputs":{"src/routeRelation.ts":{"bytesInOutput":144},"src/constants.ts":{"bytesInOutput":105},"src/segmentTesters.ts":{"bytesInOutput":1998},"src/RouteUtils.ts":{"bytesInOutput":3891},"src/index.ts":{"bytesInOutput":0},"src/getRouteUtils.ts":{"bytesInOutput":215}},"bytes":6592}}}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/routeRelation.ts","../../src/constants.ts","../../src/segmentTesters.ts","../../src/RouteUtils.ts","../../src/getRouteUtils.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// packages/route-utils/src/routeRelation.ts\n\n/**\n * Checks if two routes are related in the hierarchy.\n *\n * Routes are related if:\n * - They are exactly the same\n * - One is a parent of the other (e.g., \"users\" and \"users.list\")\n * - One is a child of the other (e.g., \"users.list\" and \"users\")\n *\n * @param route1 - First route name\n * @param route2 - Second route name\n * @returns True if routes are related, false otherwise\n *\n * @example\n * areRoutesRelated(\"users\", \"users.list\"); // true (parent-child)\n * areRoutesRelated(\"users.list\", \"users\"); // true (child-parent)\n * areRoutesRelated(\"users\", \"users\"); // true (same)\n * areRoutesRelated(\"users\", \"admin\"); // false (different branches)\n * areRoutesRelated(\"users.list\", \"users.view\"); // false (siblings)\n */\nexport function areRoutesRelated(route1: string, route2: string): boolean {\n return (\n route1 === route2 ||\n route1.startsWith(`${route2}.`) ||\n route2.startsWith(`${route1}.`)\n );\n}\n","// packages/route-utils/src/constants.ts\n\n/**\n * Maximum allowed segment length (10,000 characters)\n */\nexport const MAX_SEGMENT_LENGTH = 10_000;\n\n/**\n * Pattern for valid segment characters: alphanumeric + dot + dash + underscore\n * Uses explicit character ranges for clarity and portability.\n * Dash is placed at the end to avoid escaping (no range operator confusion).\n */\nexport const SAFE_SEGMENT_PATTERN = /^[\\w.-]+$/;\n\n/**\n * Route segment separator character\n */\nexport const ROUTE_SEGMENT_SEPARATOR = \".\";\n","// packages/route-utils/src/segmentTesters.ts\n\nimport {\n MAX_SEGMENT_LENGTH,\n ROUTE_SEGMENT_SEPARATOR,\n SAFE_SEGMENT_PATTERN,\n} from \"./constants\";\n\nimport type { SegmentTestFunction } from \"./types\";\nimport type { State } from \"@real-router/types\";\n\n/**\n * Escapes special RegExp characters in a string.\n * Handles all RegExp metacharacters including dash in character classes.\n *\n * @param str - String to escape\n * @returns Escaped string safe for RegExp construction\n * @internal\n */\nconst escapeRegExp = (str: string): string =>\n str.replaceAll(/[$()*+.?[\\\\\\]^{|}-]/g, String.raw`\\$&`);\n\n/**\n * Creates a segment tester function with specified start and end patterns.\n * This is a factory function that produces the actual test functions.\n *\n * @param start - RegExp pattern for start (e.g., \"^\" for startsWith)\n * @param end - RegExp pattern for end (e.g., \"$\" or dotOrEnd for specific matching)\n * @returns A test function that can check if routes match the segment pattern\n * @internal\n */\nconst makeSegmentTester = (start: string, end: string) => {\n const regexCache = new Map<string, RegExp>();\n\n /**\n * Builds a RegExp for testing segment matches.\n * Validates length and character pattern. Type and empty checks are done by caller.\n *\n * This optimizes performance by avoiding redundant checks - callers verify\n * type and empty before calling this function.\n *\n * @param segment - The segment to build a regex for (non-empty string, pre-validated)\n * @returns RegExp for testing\n * @throws {RangeError} If segment exceeds maximum length\n * @throws {TypeError} If segment contains invalid characters\n * @internal\n */\n const buildRegex = (segment: string): RegExp => {\n const cached = regexCache.get(segment);\n\n if (cached) {\n return cached;\n }\n\n // Type and empty checks are SKIPPED - caller already verified these\n\n // Length check\n if (segment.length > MAX_SEGMENT_LENGTH) {\n throw new RangeError(\n `Segment exceeds maximum length of ${MAX_SEGMENT_LENGTH} characters`,\n );\n }\n\n // Character pattern check\n if (!SAFE_SEGMENT_PATTERN.test(segment)) {\n throw new TypeError(\n `Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)`,\n );\n }\n\n const regex = new RegExp(start + escapeRegExp(segment) + end);\n\n regexCache.set(segment, regex);\n\n return regex;\n };\n\n // TypeScript cannot infer conditional return type for curried function with union return.\n // The function returns either boolean or a tester function based on whether segment is provided.\n // This is an intentional design pattern for API flexibility.\n // eslint-disable-next-line sonarjs/function-return-type\n return (route: State | string, segment?: string | null) => {\n // Extract route name, handling both string and State object inputs\n // State.name is always string by real-router type definition\n const name = typeof route === \"string\" ? route : route.name;\n\n if (typeof name !== \"string\") {\n return false;\n }\n\n // Empty route name always returns false\n if (name.length === 0) {\n return false;\n }\n\n // null always returns false (consistent behavior)\n if (segment === null) {\n return false;\n }\n\n // Currying: if no segment provided, return a tester function\n if (segment === undefined) {\n return (localSegment: string) => {\n // Type check for runtime safety (consistent with direct call)\n if (typeof localSegment !== \"string\") {\n throw new TypeError(\n `Segment must be a string, got ${typeof localSegment}`,\n );\n }\n\n // Empty string returns false (consistent with direct call)\n if (localSegment.length === 0) {\n return false;\n }\n\n // Use buildRegex (type and empty checks already done above)\n return buildRegex(localSegment).test(name);\n };\n }\n\n if (typeof segment !== \"string\") {\n // Runtime protection: TypeScript already narrows to 'string' here,\n // but we keep this check for defense against unexpected runtime values\n throw new TypeError(`Segment must be a string, got ${typeof segment}`);\n }\n\n // Empty string returns false (consistent behavior)\n if (segment.length === 0) {\n return false;\n }\n\n // Perform the actual regex test\n // buildRegex skips type and empty checks (already validated above)\n return buildRegex(segment).test(name);\n };\n};\n\n/**\n * Pattern that matches either a dot separator or end of string.\n * Used for prefix/suffix matching that respects segment boundaries.\n */\nconst dotOrEnd = `(?:${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)}|$)`;\n\n/**\n * Tests if a route name starts with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route starts with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * startsWithSegment('users.list', 'users'); // true\n * startsWithSegment('users.list', 'admin'); // false\n *\n * @example\n * // Curried form\n * const tester = startsWithSegment('users.list');\n * tester('users'); // true\n * tester('admin'); // false\n *\n * @example\n * // With State object\n * const state: State = { name: 'users.list', params: {}, path: '/users/list' };\n * startsWithSegment(state, 'users'); // true\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see endsWithSegment for suffix matching\n * @see includesSegment for anywhere matching\n */\nexport const startsWithSegment = makeSegmentTester(\n \"^\",\n dotOrEnd,\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name ends with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route ends with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * endsWithSegment('users.list', 'list'); // true\n * endsWithSegment('users.profile.edit', 'edit'); // true\n *\n * @example\n * // Curried form\n * const tester = endsWithSegment('users.list');\n * tester('list'); // true\n * tester('users'); // false\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see includesSegment for anywhere matching\n */\nexport const endsWithSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n \"$\",\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name includes the given segment anywhere in its path.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route includes segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * includesSegment('users.profile.edit', 'profile'); // true\n *\n * @example\n * // Multi-segment inclusion\n * includesSegment('a.b.c.d', 'b.c'); // true\n * includesSegment('a.b.c.d', 'a.c'); // false (must be contiguous)\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see endsWithSegment for suffix matching\n */\nexport const includesSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n dotOrEnd,\n) as SegmentTestFunction;\n","import { areRoutesRelated } from \"./routeRelation.js\";\nimport {\n startsWithSegment,\n endsWithSegment,\n includesSegment,\n} from \"./segmentTesters.js\";\n\nimport type { RouteTreeNode, SegmentTestFunction } from \"./types.js\";\n\nexport class RouteUtils {\n // ===== Static facade: segment testing =====\n\n /**\n * Tests if a route name starts with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link startsWithSegment} standalone function for details\n */\n static readonly startsWithSegment: SegmentTestFunction = startsWithSegment;\n\n /**\n * Tests if a route name ends with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link endsWithSegment} standalone function for details\n */\n static readonly endsWithSegment: SegmentTestFunction = endsWithSegment;\n\n /**\n * Tests if a route name includes the given segment anywhere in its path.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link includesSegment} standalone function for details\n */\n static readonly includesSegment: SegmentTestFunction = includesSegment;\n\n /**\n * Checks if two routes are related in the hierarchy\n * (same, parent-child, or child-parent).\n *\n * @see {@link areRoutesRelated} standalone function for details\n */\n static readonly areRoutesRelated = areRoutesRelated;\n\n // ===== Instance fields =====\n\n readonly #chainCache: Map<string, readonly string[]>;\n readonly #siblingsCache: Map<string, readonly string[]>;\n\n constructor(root: RouteTreeNode) {\n this.#chainCache = new Map();\n this.#siblingsCache = new Map();\n this.#buildAll(root, []);\n }\n\n /**\n * Returns cumulative name segments for the given route (ancestor chain without root).\n *\n * All chains are pre-computed and frozen during construction.\n *\n * @param name - Full route name (e.g. `\"users.profile\"`)\n * @returns Frozen array of cumulative segments, or `undefined` if not in tree\n *\n * @example\n * ```ts\n * utils.getChain(\"users.profile.edit\");\n * // → [\"users\", \"users.profile\", \"users.profile.edit\"]\n * ```\n */\n getChain(name: string): readonly string[] | undefined {\n return this.#chainCache.get(name);\n }\n\n /**\n * Returns non-absolute siblings of the named node (excluding itself).\n *\n * Siblings are children of the same parent, filtered by `nonAbsoluteChildren`.\n * All siblings are pre-computed and frozen during construction.\n *\n * @param name - Full route name\n * @returns Frozen array of sibling full names, or `undefined` if not found or root\n */\n getSiblings(name: string): readonly string[] | undefined {\n return this.#siblingsCache.get(name);\n }\n\n /**\n * Checks if `child` is a descendant of `parent` via string prefix comparison.\n *\n * Does not perform tree lookup — O(k) where k is the name length.\n *\n * @param child - Full name of the potential descendant\n * @param parent - Full name of the potential ancestor\n * @returns `true` if `child` starts with `parent.` (dot-separated)\n *\n * @remarks\n * Does not work with root (`\"\"`) as parent — returns `false` because\n * `\"users\".startsWith(\".\")` is `false`. This is acceptable since\n * every route in the tree is trivially a descendant of root.\n */\n isDescendantOf(child: string, parent: string): boolean {\n return child !== parent && child.startsWith(`${parent}.`);\n }\n #buildAll(node: RouteTreeNode, chain: string[]): void {\n const { fullName } = node;\n\n // Build chain: root gets [\"\"], others get cumulative segments\n if (fullName !== \"\") {\n chain.push(fullName);\n }\n\n this.#chainCache.set(\n fullName,\n Object.freeze(fullName === \"\" ? [\"\"] : [...chain]),\n );\n\n // Build siblings for all children of this node\n // Siblings = nonAbsoluteChildren excluding the child itself\n // Absolute children also get siblings (all nonAbsoluteChildren)\n const nonAbsoluteNames = node.nonAbsoluteChildren.map((c) => c.fullName);\n\n for (const child of node.nonAbsoluteChildren) {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze(nonAbsoluteNames.filter((n) => n !== child.fullName)),\n );\n }\n\n // Absolute children: their siblings are ALL nonAbsoluteChildren\n for (const child of node.children.values()) {\n if (!this.#siblingsCache.has(child.fullName) && child.fullName !== \"\") {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze([...nonAbsoluteNames]),\n );\n }\n }\n\n // Recurse into all children (including absolute)\n for (const child of node.children.values()) {\n this.#buildAll(child, chain);\n }\n\n // Restore chain for sibling traversal\n if (fullName !== \"\") {\n chain.pop();\n }\n }\n}\n","import { RouteUtils } from \"./RouteUtils.js\";\n\nimport type { RouteTreeNode } from \"./types.js\";\n\nconst cache = new WeakMap<RouteTreeNode, RouteUtils>();\n\nexport function getRouteUtils(root: RouteTreeNode): RouteUtils {\n let utils = cache.get(root);\n\n if (utils === undefined) {\n utils = new RouteUtils(root);\n cache.set(root, utils);\n }\n\n return utils;\n}\n"]}
1
+ {"version":3,"sources":["../../src/routeRelation.ts","../../src/constants.ts","../../src/segmentTesters.ts","../../src/RouteUtils.ts","../../src/getRouteUtils.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// packages/route-utils/src/routeRelation.ts\n\n/**\n * Checks if two routes are related in the hierarchy.\n *\n * Routes are related if:\n * - They are exactly the same\n * - One is a parent of the other (e.g., \"users\" and \"users.list\")\n * - One is a child of the other (e.g., \"users.list\" and \"users\")\n *\n * @param route1 - First route name\n * @param route2 - Second route name\n * @returns True if routes are related, false otherwise\n *\n * @example\n * areRoutesRelated(\"users\", \"users.list\"); // true (parent-child)\n * areRoutesRelated(\"users.list\", \"users\"); // true (child-parent)\n * areRoutesRelated(\"users\", \"users\"); // true (same)\n * areRoutesRelated(\"users\", \"admin\"); // false (different branches)\n * areRoutesRelated(\"users.list\", \"users.view\"); // false (siblings)\n */\nexport function areRoutesRelated(route1: string, route2: string): boolean {\n return (\n route1 === route2 ||\n route1.startsWith(`${route2}.`) ||\n route2.startsWith(`${route1}.`)\n );\n}\n","// packages/route-utils/src/constants.ts\n\n/**\n * Maximum allowed segment length (10,000 characters)\n */\nexport const MAX_SEGMENT_LENGTH = 10_000;\n\n/**\n * Pattern for valid segment characters: alphanumeric + dot + dash + underscore\n * Uses explicit character ranges for clarity and portability.\n * Dash is placed at the end to avoid escaping (no range operator confusion).\n */\nexport const SAFE_SEGMENT_PATTERN = /^[\\w.-]+$/;\n\n/**\n * Route segment separator character\n */\nexport const ROUTE_SEGMENT_SEPARATOR = \".\";\n","// packages/route-utils/src/segmentTesters.ts\n\nimport {\n MAX_SEGMENT_LENGTH,\n ROUTE_SEGMENT_SEPARATOR,\n SAFE_SEGMENT_PATTERN,\n} from \"./constants\";\n\nimport type { SegmentTestFunction } from \"./types\";\nimport type { State } from \"@real-router/types\";\n\n/**\n * Escapes special RegExp characters in a string.\n * Handles all RegExp metacharacters including dash in character classes.\n *\n * @param str - String to escape\n * @returns Escaped string safe for RegExp construction\n * @internal\n */\nconst escapeRegExp = (str: string): string =>\n str.replaceAll(/[$()*+.?[\\\\\\]^{|}-]/g, String.raw`\\$&`);\n\n/**\n * Creates a segment tester function with specified start and end patterns.\n * This is a factory function that produces the actual test functions.\n *\n * @param start - RegExp pattern for start (e.g., \"^\" for startsWith)\n * @param end - RegExp pattern for end (e.g., \"$\" or dotOrEnd for specific matching)\n * @returns A test function that can check if routes match the segment pattern\n * @internal\n */\nconst makeSegmentTester = (start: string, end: string) => {\n const regexCache = new Map<string, RegExp>();\n\n /**\n * Builds a RegExp for testing segment matches.\n * Validates length and character pattern. Type and empty checks are done by caller.\n *\n * This optimizes performance by avoiding redundant checks - callers verify\n * type and empty before calling this function.\n *\n * @param segment - The segment to build a regex for (non-empty string, pre-validated)\n * @returns RegExp for testing\n * @throws {RangeError} If segment exceeds maximum length\n * @throws {TypeError} If segment contains invalid characters\n * @internal\n */\n const buildRegex = (segment: string): RegExp => {\n const cached = regexCache.get(segment);\n\n if (cached) {\n return cached;\n }\n\n // Type and empty checks are SKIPPED - caller already verified these\n\n // Length check\n if (segment.length > MAX_SEGMENT_LENGTH) {\n throw new RangeError(\n `Segment exceeds maximum length of ${MAX_SEGMENT_LENGTH} characters`,\n );\n }\n\n // Character pattern check\n if (!SAFE_SEGMENT_PATTERN.test(segment)) {\n throw new TypeError(\n `Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)`,\n );\n }\n\n const regex = new RegExp(start + escapeRegExp(segment) + end);\n\n regexCache.set(segment, regex);\n\n return regex;\n };\n\n // TypeScript cannot infer conditional return type for curried function with union return.\n // The function returns either boolean or a tester function based on whether segment is provided.\n // This is an intentional design pattern for API flexibility.\n // eslint-disable-next-line sonarjs/function-return-type\n return (route: State | string, segment?: string | null) => {\n // Extract route name, handling both string and State object inputs\n // State.name is always string by real-router type definition\n const name = typeof route === \"string\" ? route : route.name;\n\n if (typeof name !== \"string\") {\n return false;\n }\n\n // Empty route name always returns false\n if (name.length === 0) {\n return false;\n }\n\n // null always returns false (consistent behavior)\n if (segment === null) {\n return false;\n }\n\n // Currying: if no segment provided, return a tester function\n if (segment === undefined) {\n return (localSegment: string) => {\n // Type check for runtime safety (consistent with direct call)\n if (typeof localSegment !== \"string\") {\n throw new TypeError(\n `Segment must be a string, got ${typeof localSegment}`,\n );\n }\n\n // Empty string returns false (consistent with direct call)\n if (localSegment.length === 0) {\n return false;\n }\n\n // Use buildRegex (type and empty checks already done above)\n return buildRegex(localSegment).test(name);\n };\n }\n\n if (typeof segment !== \"string\") {\n // Runtime protection: TypeScript already narrows to 'string' here,\n // but we keep this check for defense against unexpected runtime values\n throw new TypeError(`Segment must be a string, got ${typeof segment}`);\n }\n\n // Empty string returns false (consistent behavior)\n if (segment.length === 0) {\n return false;\n }\n\n // Perform the actual regex test\n // buildRegex skips type and empty checks (already validated above)\n return buildRegex(segment).test(name);\n };\n};\n\n/**\n * Pattern that matches either a dot separator or end of string.\n * Used for prefix/suffix matching that respects segment boundaries.\n */\nconst dotOrEnd = `(?:${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)}|$)`;\n\n/**\n * Tests if a route name starts with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route starts with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * startsWithSegment('users.list', 'users'); // true\n * startsWithSegment('users.list', 'admin'); // false\n *\n * @example\n * // Curried form\n * const tester = startsWithSegment('users.list');\n * tester('users'); // true\n * tester('admin'); // false\n *\n * @example\n * // With State object\n * const state: State = { name: 'users.list', params: {}, path: '/users/list' };\n * startsWithSegment(state, 'users'); // true\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see endsWithSegment for suffix matching\n * @see includesSegment for anywhere matching\n */\nexport const startsWithSegment = makeSegmentTester(\n \"^\",\n dotOrEnd,\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name ends with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route ends with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * endsWithSegment('users.list', 'list'); // true\n * endsWithSegment('users.profile.edit', 'edit'); // true\n *\n * @example\n * // Curried form\n * const tester = endsWithSegment('users.list');\n * tester('list'); // true\n * tester('users'); // false\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see includesSegment for anywhere matching\n */\nexport const endsWithSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n \"$\",\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name includes the given segment anywhere in its path.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route includes segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * includesSegment('users.profile.edit', 'profile'); // true\n *\n * @example\n * // Multi-segment inclusion\n * includesSegment('a.b.c.d', 'b.c'); // true\n * includesSegment('a.b.c.d', 'a.c'); // false (must be contiguous)\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see endsWithSegment for suffix matching\n */\nexport const includesSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n dotOrEnd,\n) as SegmentTestFunction;\n","import { areRoutesRelated } from \"./routeRelation.js\";\nimport {\n startsWithSegment,\n endsWithSegment,\n includesSegment,\n} from \"./segmentTesters.js\";\n\nimport type { RouteTreeNode, SegmentTestFunction } from \"./types.js\";\n\nexport class RouteUtils {\n // ===== Static facade: segment testing =====\n\n /**\n * Tests if a route name starts with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link startsWithSegment} standalone function for details\n */\n static readonly startsWithSegment: SegmentTestFunction = startsWithSegment;\n\n /**\n * Tests if a route name ends with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link endsWithSegment} standalone function for details\n */\n static readonly endsWithSegment: SegmentTestFunction = endsWithSegment;\n\n /**\n * Tests if a route name includes the given segment anywhere in its path.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link includesSegment} standalone function for details\n */\n static readonly includesSegment: SegmentTestFunction = includesSegment;\n\n /**\n * Checks if two routes are related in the hierarchy\n * (same, parent-child, or child-parent).\n *\n * @see {@link areRoutesRelated} standalone function for details\n */\n static readonly areRoutesRelated = areRoutesRelated;\n\n // ===== Instance fields =====\n\n readonly #chainCache: Map<string, readonly string[]>;\n readonly #siblingsCache: Map<string, readonly string[]>;\n\n constructor(root: RouteTreeNode) {\n this.#chainCache = new Map();\n this.#siblingsCache = new Map();\n this.#buildAll(root, []);\n }\n\n /**\n * Returns cumulative name segments for the given route (ancestor chain without root).\n *\n * All chains are pre-computed and frozen during construction.\n *\n * @param name - Full route name (e.g. `\"users.profile\"`)\n * @returns Frozen array of cumulative segments, or `undefined` if not in tree\n *\n * @example\n * ```ts\n * utils.getChain(\"users.profile.edit\");\n * // → [\"users\", \"users.profile\", \"users.profile.edit\"]\n * ```\n */\n getChain(name: string): readonly string[] | undefined {\n return this.#chainCache.get(name);\n }\n\n /**\n * Returns non-absolute siblings of the named node (excluding itself).\n *\n * Siblings are children of the same parent, filtered by `nonAbsoluteChildren`.\n * All siblings are pre-computed and frozen during construction.\n *\n * @param name - Full route name\n * @returns Frozen array of sibling full names, or `undefined` if not found or root\n */\n getSiblings(name: string): readonly string[] | undefined {\n return this.#siblingsCache.get(name);\n }\n\n /**\n * Checks if `child` is a descendant of `parent` via string prefix comparison.\n *\n * Does not perform tree lookup — O(k) where k is the name length.\n *\n * @param child - Full name of the potential descendant\n * @param parent - Full name of the potential ancestor\n * @returns `true` if `child` starts with `parent.` (dot-separated)\n *\n * @remarks\n * Does not work with root (`\"\"`) as parent — returns `false` because\n * `\"users\".startsWith(\".\")` is `false`. This is acceptable since\n * every route in the tree is trivially a descendant of root.\n */\n isDescendantOf(child: string, parent: string): boolean {\n return child !== parent && child.startsWith(`${parent}.`);\n }\n #buildAll(node: RouteTreeNode, chain: string[]): void {\n const { fullName } = node;\n\n // Build chain: root gets [\"\"], others get cumulative segments\n if (fullName !== \"\") {\n chain.push(fullName);\n }\n\n this.#chainCache.set(\n fullName,\n Object.freeze(fullName === \"\" ? [\"\"] : [...chain]),\n );\n\n // Build siblings for all children of this node\n // Siblings = nonAbsoluteChildren excluding the child itself\n // Absolute children also get siblings (all nonAbsoluteChildren)\n const nonAbsoluteNames = node.nonAbsoluteChildren.map(\n (child) => child.fullName,\n );\n\n for (const child of node.nonAbsoluteChildren) {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze(\n nonAbsoluteNames.filter((name) => name !== child.fullName),\n ),\n );\n }\n\n // Absolute children: their siblings are ALL nonAbsoluteChildren\n for (const child of node.children.values()) {\n if (!this.#siblingsCache.has(child.fullName) && child.fullName !== \"\") {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze([...nonAbsoluteNames]),\n );\n }\n }\n\n // Recurse into all children (including absolute)\n for (const child of node.children.values()) {\n this.#buildAll(child, chain);\n }\n\n // Restore chain for sibling traversal\n if (fullName !== \"\") {\n chain.pop();\n }\n }\n}\n","import { RouteUtils } from \"./RouteUtils.js\";\n\nimport type { RouteTreeNode } from \"./types.js\";\n\nconst cache = new WeakMap<RouteTreeNode, RouteUtils>();\n\nexport function getRouteUtils(root: RouteTreeNode): RouteUtils {\n let utils = cache.get(root);\n\n if (utils === undefined) {\n utils = new RouteUtils(root);\n cache.set(root, utils);\n }\n\n return utils;\n}\n"]}
@@ -1 +1 @@
1
- {"inputs":{"src/routeRelation.ts":{"bytes":994,"imports":[],"format":"esm"},"src/constants.ts":{"bytes":515,"imports":[],"format":"esm"},"src/segmentTesters.ts":{"bytes":8570,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"}],"format":"esm"},"src/RouteUtils.ts":{"bytes":4807,"imports":[{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"}],"format":"esm"},"src/getRouteUtils.ts":{"bytes":365,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"}],"format":"esm"},"src/index.ts":{"bytes":310,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"},{"path":"src/getRouteUtils.ts","kind":"import-statement","original":"./getRouteUtils.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"},{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"}],"format":"esm"}},"outputs":{"dist/esm/index.mjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":19261},"dist/esm/index.mjs":{"imports":[],"exports":["RouteUtils","areRoutesRelated","endsWithSegment","getRouteUtils","includesSegment","startsWithSegment"],"entryPoint":"src/index.ts","inputs":{"src/routeRelation.ts":{"bytesInOutput":144},"src/constants.ts":{"bytesInOutput":105},"src/segmentTesters.ts":{"bytesInOutput":1998},"src/RouteUtils.ts":{"bytesInOutput":3845},"src/index.ts":{"bytesInOutput":0},"src/getRouteUtils.ts":{"bytesInOutput":215}},"bytes":6546}}}
1
+ {"inputs":{"src/routeRelation.ts":{"bytes":994,"imports":[],"format":"esm"},"src/constants.ts":{"bytes":515,"imports":[],"format":"esm"},"src/segmentTesters.ts":{"bytes":8570,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"}],"format":"esm"},"src/RouteUtils.ts":{"bytes":4855,"imports":[{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"}],"format":"esm"},"src/getRouteUtils.ts":{"bytes":365,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"}],"format":"esm"},"src/index.ts":{"bytes":310,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"},{"path":"src/getRouteUtils.ts","kind":"import-statement","original":"./getRouteUtils.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"},{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"}],"format":"esm"}},"outputs":{"dist/esm/index.mjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":19334},"dist/esm/index.mjs":{"imports":[],"exports":["RouteUtils","areRoutesRelated","endsWithSegment","getRouteUtils","includesSegment","startsWithSegment"],"entryPoint":"src/index.ts","inputs":{"src/routeRelation.ts":{"bytesInOutput":144},"src/constants.ts":{"bytesInOutput":105},"src/segmentTesters.ts":{"bytesInOutput":1998},"src/RouteUtils.ts":{"bytesInOutput":3891},"src/index.ts":{"bytesInOutput":0},"src/getRouteUtils.ts":{"bytesInOutput":215}},"bytes":6592}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/route-utils",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "commonjs",
5
5
  "description": "Cached read-only query API for route tree structure",
6
6
  "main": "./dist/cjs/index.js",
@@ -45,7 +45,7 @@
45
45
  "route-tree": "^0.3.4"
46
46
  },
47
47
  "dependencies": {
48
- "@real-router/types": "^0.24.0"
48
+ "@real-router/types": "^0.27.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "tsup",
@@ -53,6 +53,7 @@
53
53
  "lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0 --no-error-on-unmatched-pattern",
54
54
  "test": "vitest run",
55
55
  "test:properties": "vitest --config vitest.config.properties.mts --run",
56
- "bench": "NODE_OPTIONS='--expose-gc --max-old-space-size=4096' npx tsx tests/benchmarks/index.ts"
56
+ "bench": "NODE_OPTIONS='--expose-gc --max-old-space-size=4096' npx tsx tests/benchmarks/index.ts",
57
+ "build:dist-only": "tsup"
57
58
  }
58
59
  }
package/src/RouteUtils.ts CHANGED
@@ -117,12 +117,16 @@ export class RouteUtils {
117
117
  // Build siblings for all children of this node
118
118
  // Siblings = nonAbsoluteChildren excluding the child itself
119
119
  // Absolute children also get siblings (all nonAbsoluteChildren)
120
- const nonAbsoluteNames = node.nonAbsoluteChildren.map((c) => c.fullName);
120
+ const nonAbsoluteNames = node.nonAbsoluteChildren.map(
121
+ (child) => child.fullName,
122
+ );
121
123
 
122
124
  for (const child of node.nonAbsoluteChildren) {
123
125
  this.#siblingsCache.set(
124
126
  child.fullName,
125
- Object.freeze(nonAbsoluteNames.filter((n) => n !== child.fullName)),
127
+ Object.freeze(
128
+ nonAbsoluteNames.filter((name) => name !== child.fullName),
129
+ ),
126
130
  );
127
131
  }
128
132