{"version":3,"file":"index.mjs","names":["obj","isState","#router","#browser","#urlPrefix","#removeStartInterceptor","#removeExtensions","#lifecycle","#warnHashIgnored"],"sources":["../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/history-api.ts","../../../../shared/browser-env/utils.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../../shared/browser-env/state-guard.ts","../../../../shared/browser-env/popstate-utils.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/safe-browser.ts","../../../../shared/browser-env/popstate-handler.ts","../../../../shared/browser-env/url-context.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/plugin-utils.ts","../../src/constants.ts","../../src/hash-utils.ts","../../src/plugin.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["export const isBrowserEnvironment = (): boolean =>\n  typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","import type { HistoryBrowser } from \"./types.js\";\n\nexport const pushState = (state: unknown, path: string): void => {\n  globalThis.history.pushState(state, \"\", path);\n};\n\nexport const replaceState = (state: unknown, path: string): void => {\n  globalThis.history.replaceState(state, \"\", path);\n};\n\nexport const addPopstateListener: HistoryBrowser[\"addPopstateListener\"] = (\n  fn,\n) => {\n  globalThis.addEventListener(\"popstate\", fn);\n\n  return () => {\n    globalThis.removeEventListener(\"popstate\", fn);\n  };\n};\n\nexport const addHashChangeListener: HistoryBrowser[\"addHashChangeListener\"] = (\n  fn,\n) => {\n  globalThis.addEventListener(\"hashchange\", fn);\n\n  return () => {\n    globalThis.removeEventListener(\"hashchange\", fn);\n  };\n};\n\nexport const getHash = (): string => globalThis.location.hash;\n\nexport const getState: NonNullable<HistoryBrowser[\"getState\"]> = () =>\n  globalThis.history.state;\n","/**\n * Normalizes base path to canonical form: leading slash, no trailing slash,\n * no repeated slashes. Isolated \"/\" collapses to \"\".\n *\n * @example\n * normalizeBase(\"app\")     // \"/app\"\n * normalizeBase(\"/app/\")   // \"/app\"\n * normalizeBase(\"//app//\") // \"/app\"\n * normalizeBase(\"\")        // \"\"\n * normalizeBase(\"/\")       // \"\"\n */\nexport function normalizeBase(base: string): string {\n  if (!base) {\n    return base;\n  }\n\n  let result = base.replaceAll(/\\/+/g, \"/\");\n\n  if (!result.startsWith(\"/\")) {\n    result = `/${result}`;\n  }\n\n  if (result.length > 1 && result.endsWith(\"/\")) {\n    result = result.slice(0, -1);\n  }\n\n  return result === \"/\" ? \"\" : result;\n}\n\nexport const safelyEncodePath = (path: string): string => {\n  try {\n    return encodeURI(decodeURI(path));\n  } catch (error) {\n    console.warn(`[browser-env] Could not encode path \"${path}\"`, error);\n\n    return path;\n  }\n};\n","import type { HistoryBrowser } from \"./types.js\";\n\nconst NOOP = (): void => {};\n\nexport const createWarnOnce = (context: string) => {\n  let hasWarned = false;\n\n  return (method: string): void => {\n    if (!hasWarned) {\n      console.warn(\n        `[browser-env] Browser API is running in a non-browser environment (context: \"${context}\"). ` +\n          `Method \"${method}\" is a no-op. ` +\n          `This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`,\n      );\n      hasWarned = true;\n    }\n  };\n};\n\nexport const createHistoryFallbackBrowser = (\n  context: string,\n): HistoryBrowser => {\n  const warnOnce = createWarnOnce(context);\n\n  return {\n    pushState: () => {\n      warnOnce(\"pushState\");\n    },\n    replaceState: () => {\n      warnOnce(\"replaceState\");\n    },\n    addPopstateListener: () => {\n      warnOnce(\"addPopstateListener\");\n\n      return NOOP;\n    },\n    addHashChangeListener: () => {\n      warnOnce(\"addHashChangeListener\");\n\n      return NOOP;\n    },\n    getHash: () => {\n      warnOnce(\"getHash\");\n\n      return \"\";\n    },\n  };\n};\n","// shared/browser-env/state-guard.ts\n\nimport type { Params, State } from \"@real-router/core\";\n\n/**\n * `isStateStrict` — the `history.state` shape guard, re-exported as `isState` by\n * browser-plugin and hash-plugin and consumed by `popstate-utils`.\n *\n * ─────────────────────────────────────────────────────────────────────────────\n * LOCKSTEP TWIN (M1 — dissolution of the former private `type-guards` package).\n * The state guard's transitive closure — `isRequiredFields`, `isRouteName`,\n * `isParams` and its serialization machinery, plus the two route-name\n * constants — is DUPLICATED here from\n * `packages/validation-plugin/src/type-guards/` because the two homes share no\n * common dependency (browser-env is a symlinked shared source consumed by the\n * URL plugins; it must not depend on validation-plugin). This mirrors the\n * `getTypeDescription` twin between `type-guards` and `engine` (#903/#1052): one\n * behavioural contract, two byte-identical copies. **Any change to the guard\n * semantics here MUST be mirrored in validation-plugin's copy and vice versa.**\n * Only `isStateStrict` is exported — the helpers are module-private.\n * ─────────────────────────────────────────────────────────────────────────────\n */\n\n// ── Route-name constants (twin of type-guards internal/router-error.ts) ──────\n\n/**\n * Pattern for complete route validation (all segments at once).\n * Each segment must start with letter/underscore, followed by\n * alphanumeric/hyphen/underscore; segments joined by dots.\n */\n// eslint-disable-next-line security/detect-unsafe-regex -- safe: each `(?:\\.…)*` repetition is anchored by a literal `.`, which `[\\w-]*` cannot match, so the inner/outer quantifiers consume disjoint classes — no catastrophic backtracking (safe-regex over-flags the nested `*`).\nconst FULL_ROUTE_PATTERN = /^[A-Z_a-z][\\w-]*(?:\\.[A-Z_a-z][\\w-]*)*$/;\n\n/**\n * Maximum route name length to prevent DoS and performance issues.\n * Technical limit, not a business constraint.\n */\nconst MAX_ROUTE_NAME_LENGTH = 10_000;\n\n/**\n * Type guard that checks if a value is a valid route name (twin of\n * type-guards guards/routes.ts `isRouteName`). Empty string is the root node;\n * `@@`-prefixed system routes bypass the pattern.\n */\nfunction isRouteName(name: unknown): name is string {\n  if (typeof name !== \"string\") {\n    return false;\n  }\n\n  // Empty string is valid (represents root node)\n  if (name === \"\") {\n    return true;\n  }\n\n  // Too long is invalid\n  if (name.length > MAX_ROUTE_NAME_LENGTH) {\n    return false;\n  }\n\n  // System routes are valid (bypass pattern validation)\n  if (name.startsWith(\"@@\")) {\n    return true;\n  }\n\n  // Regular routes must match pattern\n  return FULL_ROUTE_PATTERN.test(name);\n}\n\n// ── isParams + serialization machinery (twin of type-guards guards/params.ts) ─\n\n/**\n * Is `value` an array, or a plain object (`Object.prototype` / `null` prototype)?\n * Class instances (Date, RegExp, Map, Set, ...) are not plain containers.\n */\nfunction isPlainContainer(value: object): boolean {\n  if (Array.isArray(value)) {\n    return true;\n  }\n\n  const proto = Object.getPrototypeOf(value) as object | null;\n\n  return proto === null || proto === Object.prototype;\n}\n\n/**\n * Pushes every child of an array or plain object onto the work-stack.\n */\nfunction pushChildren(value: object, stack: unknown[]): void {\n  const children = Array.isArray(value) ? value : Object.values(value);\n\n  for (const child of children) {\n    stack.push(child);\n  }\n}\n\n/**\n * Is `value` a serializable primitive leaf? `string` and `boolean` always are; a\n * `number` only if finite. Everything else (function, symbol, bigint) is not.\n */\nfunction isSerializableLeaf(value: unknown): boolean {\n  const type = typeof value;\n\n  if (type === \"string\" || type === \"boolean\") {\n    return true;\n  }\n\n  if (type === \"number\") {\n    return Number.isFinite(value);\n  }\n\n  return false;\n}\n\n/**\n * Marker pushed onto the work-stack after a container's children; popping it\n * means that container's subtree is validated. A module-private class, so user\n * data can never be mistaken for it.\n */\nclass SubtreeExit {\n  constructor(readonly container: object) {}\n}\n\n/**\n * Inspects one container popped from the work-stack. Rejects cycles (a back-edge\n * to a container still on the current DFS path) and class instances; skips\n * already-validated shared references; otherwise marks the container on-path and\n * queues its children plus a {@link SubtreeExit} marker.\n *\n * @returns false only when the container is invalid (cycle or class instance)\n */\nfunction visitContainer(\n  value: object,\n  stack: unknown[],\n  onPath: WeakSet<object>,\n  done: WeakSet<object>,\n): boolean {\n  if (onPath.has(value)) {\n    return false; // back-edge → genuine circular reference\n  }\n\n  if (done.has(value)) {\n    return true; // shared reference / diamond — subtree already validated\n  }\n\n  if (!isPlainContainer(value)) {\n    return false; // instance of a class\n  }\n\n  onPath.add(value);\n  stack.push(new SubtreeExit(value));\n  pushChildren(value, stack);\n\n  return true;\n}\n\n/**\n * Internal helper to check if value is serializable (no circular refs, functions,\n * instances). Iterative (explicit work-stack) rather than recursive so it scales\n * to any nesting depth (#901); on-path (DFS gray/black) cycle detection accepts\n * shared references / diamonds while rejecting genuine cycles (#786).\n */\nfunction isSerializable(root: unknown): boolean {\n  const stack: unknown[] = [root];\n  const onPath = new WeakSet<object>();\n  const done = new WeakSet<object>();\n\n  while (stack.length > 0) {\n    const value = stack.pop();\n\n    // Subtree fully processed: leave the current path, mark as validated.\n    if (value instanceof SubtreeExit) {\n      onPath.delete(value.container);\n      done.add(value.container);\n\n      continue;\n    }\n\n    // null/undefined are serializable (JSON.stringify handles them)\n    if (value === null || value === undefined) {\n      continue;\n    }\n\n    // Arrays and plain objects (typeof null is \"object\", handled above).\n    if (typeof value === \"object\") {\n      if (!visitContainer(value, stack, onPath, done)) {\n        return false;\n      }\n\n      continue;\n    }\n\n    // Primitive leaf: string / boolean / finite number pass; function, symbol,\n    // bigint, and NaN/Infinity do not.\n    if (!isSerializableLeaf(value)) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\n/**\n * Fast path check for primitive values (no recursion needed).\n * Returns true if primitive, false if needs deeper inspection.\n */\nfunction isPrimitiveValue(value: unknown): boolean {\n  if (value === null || value === undefined) {\n    return true;\n  }\n\n  const type = typeof value;\n\n  if (type === \"string\" || type === \"boolean\") {\n    return true;\n  }\n\n  if (type === \"number\") {\n    return Number.isFinite(value);\n  }\n\n  // object, array, function, symbol — need deeper check\n  return false;\n}\n\n/**\n * Type guard for Params object. Validates that all values are serializable\n * (primitives, arrays, nested arrays, or nested objects). Rejects circular\n * references, functions, symbols, and class instances. Two-phase: a fast path\n * for flat primitive objects, a recursive slow path for nested structures.\n * Getter-safe (#1052): a throwing `[[Get]]` during the walk → not valid params.\n */\nfunction isParams(value: unknown): value is Params {\n  try {\n    return isParamsUnsafe(value);\n  } catch {\n    return false;\n  }\n}\n\nfunction isParamsUnsafe(value: unknown): value is Params {\n  // Reject null, undefined, and arrays (must be a plain object)\n  if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n    return false;\n  }\n\n  // Reject objects with custom prototype (e.g., Object.create(proto), class instances)\n  const proto = Object.getPrototypeOf(value) as object | null;\n\n  if (proto !== null && proto !== Object.prototype) {\n    return false;\n  }\n\n  // Phase 1: Fast path for flat objects (all values are primitives)\n  let needsDeepCheck = false;\n\n  for (const key in value) {\n    // Skip inherited properties (defensive against Object.prototype pollution).\n    if (!Object.hasOwn(value, key)) {\n      continue;\n    }\n\n    const val = (value as Record<string, unknown>)[key];\n\n    if (!isPrimitiveValue(val)) {\n      const type = typeof val;\n\n      if (type === \"function\" || type === \"symbol\") {\n        return false; // Early reject\n      }\n\n      needsDeepCheck = true;\n\n      break; // Exit fast path, proceed to slow path\n    }\n  }\n\n  // Fast path: all primitives, valid params\n  if (!needsDeepCheck) {\n    return true;\n  }\n\n  // Phase 2: Slow path — full recursive validation\n  return isSerializable(value);\n}\n\n// ── isRequiredFields + isStateStrict (twin of meta-fields.ts / state.ts) ─────\n\n/**\n * Type guard helper that checks if required State fields have valid types\n * (twin of type-guards internal/meta-fields.ts `isRequiredFields`).\n */\nfunction isRequiredFields(obj: Record<string, unknown>): boolean {\n  return (\n    isRouteName(obj.name) &&\n    typeof obj.path === \"string\" &&\n    isParams(obj.params)\n  );\n}\n\n/**\n * Type guard for State. Performs the required-field check (`name` via\n * `isRouteName`, `path` is a string, `params` via `isParams`) — the \"Strict\" in\n * the name is historical: there is no deeper meta-field validation, and `meta.id`\n * is intentionally NOT type-checked (history.state restores serialize it as a\n * string). Re-exported as `isState` by the browser and hash plugins for\n * validating `history.state`.\n *\n * @param value - Value to check\n * @returns true if value has the required State fields with valid types\n *\n * @example\n * isStateStrict({ name: 'home', params: {}, path: '/' }); // true\n * isStateStrict({ name: 'home', params: 'invalid', path: '/' }); // false\n */\nexport function isStateStrict<P extends Params = Params>(\n  value: unknown,\n): value is State<P> {\n  // Basic structure check\n  if (typeof value !== \"object\" || value === null) {\n    return false;\n  }\n\n  const obj = value as Record<string, unknown>;\n\n  // Check required fields and their types\n  return isRequiredFields(obj);\n}\n","import { isStateStrict as isState } from \"./state-guard\";\n\nimport type { Browser } from \"./types.js\";\nimport type { State, Params } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Resolves the popstate event into a navigation-ready `State`.\n *\n * - If `history.state` is a valid router state ({name, params, path} written\n *   by browser-plugin/hash-plugin during their previous navigation), it is\n *   the source of truth — synthesize a fully-typed `State` from it via\n *   `api.makeState`. The synthesized `transition`/`context` fields are\n *   placeholders; the navigation pipeline (`completeTransition` and plugin\n *   claim writes) replaces them.\n * - Otherwise (e.g. manually entered URL with no recorded state), fall back\n *   to `api.matchPath(location)`. `location` is the route location the caller\n *   captured when the popstate event fired — each plugin derives it from its\n *   own `browser.getLocation()`, and both return a path the matcher understands\n *   (browser-plugin: the History pathname; hash-plugin: the hash route via\n *   `buildHashLocation(location.hash, ...)`), so the fallback works for both.\n *   (#760)\n * - `undefined` when neither path produces a match.\n *\n * The caller passes the location it snapshotted at event time rather than\n * letting this function re-read `browser.getLocation()`: a deferred popstate\n * is processed only after the in-flight navigation's `replaceState` has\n * already overwritten the live location, so a late read would resolve the\n * wrong target (#757).\n *\n * Replaces the previous `{ name, params }` shape so the caller can hand\n * the State directly to `router.navigateToState(state, opts)` and skip\n * the redundant `forwardState`/`buildPath` round-trip in\n * `buildNavigateState` (issue #525).\n *\n * Accepts `HashChangeEvent` too (#759): a `hashchange` carries no history\n * `state`, so it always resolves via the `matchPath(location)` fallback — the\n * correct source of truth for an external fragment change, where the URL, not\n * a plugin-recorded entry, defines the target.\n */\nexport function getRouteFromEvent(\n  evt: PopStateEvent | HashChangeEvent,\n  api: PluginApi,\n  location: string,\n): State | undefined {\n  const state: unknown = \"state\" in evt ? evt.state : undefined;\n\n  if (isState(state)) {\n    return api.makeState(state.name, state.params, state.path);\n  }\n\n  return api.matchPath(location);\n}\n\n/**\n * Updates browser state (pushState or replaceState)\n *\n * @param state - Router state\n * @param url - URL to set\n * @param replace - Whether to replace instead of push\n * @param browser - Browser API instance\n */\nexport function updateBrowserState(\n  state: State,\n  url: string,\n  replace: boolean,\n  browser: Browser,\n): void {\n  const historyState = {\n    name: state.name,\n    params: state.params,\n    path: state.path,\n  };\n\n  if (replace) {\n    browser.replaceState(historyState, url);\n  } else {\n    browser.pushState(historyState, url);\n  }\n}\n\n/**\n * Creates a `updateBrowserState` closure that reuses a single mutable buffer\n * across calls instead of allocating a fresh `{ name, params, path }` object\n * per push/replace.\n *\n * Why: Browsers structured-clone `history.state` synchronously inside\n * `pushState`/`replaceState`, so the caller never sees the buffer escape —\n * it can be safely overwritten before the next call. Eliminates one\n * allocation per navigation on the hot path.\n *\n * Each plugin instance must own its own buffer (do not share across plugins).\n */\nexport function createUpdateBrowserState(): (\n  state: State,\n  url: string,\n  replace: boolean,\n  browser: Browser,\n) => void {\n  const buffer = {\n    name: \"\",\n    params: {} as Params,\n    path: \"\",\n  };\n\n  return (state, url, replace, browser) => {\n    buffer.name = state.name;\n    buffer.params = state.params;\n    buffer.path = state.path;\n\n    if (replace) {\n      browser.replaceState(buffer, url);\n    } else {\n      browser.pushState(buffer, url);\n    }\n  };\n}\n\n/**\n * True when a popstate-success `replaceState` would be a value-level no-op and\n * can be skipped to avoid a redundant `updateForSameDocumentNavigation` Blink\n * event on back/forward (#1353).\n *\n * On a back/forward to an entry the plugin itself recorded, the browser has\n * ALREADY restored the identical `{name, params, path}` into `history.state`\n * and the matching URL before firing popstate — re-writing them costs a full\n * Blink history event for zero value change.\n *\n * Returns `false` (→ keep the write) for every divergence that makes the write\n * load-bearing, so the correctness cases stay covered:\n *   - redirect            → resolved name/params differ from the restored entry\n *   - path normalization  → resolved path differs (e.g. trailing slash)\n *   - corrupted / missing `history.state` → fails `isState`\n *   - custom `Browser` without a state reader → `getState` absent (opt-in)\n *\n * URL equality is not re-checked: the resolved path is compared directly, and\n * the popstate fragment is sampled FROM the live location, so the committed URL\n * already matches by construction. The deferred-popstate replay (#757) is\n * unaffected too — it reads the event's own snapshotted state/location, never\n * the live entry this write would commit.\n */\nexport function canSkipPopstateHistoryWrite(\n  toState: State,\n  browser: Browser,\n  areStatesEqual: (\n    state1: State,\n    state2: State,\n    ignoreQueryParams: boolean,\n  ) => boolean,\n): boolean {\n  if (!browser.getState) {\n    return false;\n  }\n\n  const live = browser.getState();\n\n  return (\n    isState(live) &&\n    live.path === toState.path &&\n    areStatesEqual(toState, live as unknown as State, false)\n  );\n}\n","export interface OptionRule<T> {\n  validate: (value: T) => string | null;\n}\n\nexport type OptionRules<T extends object> = {\n  [K in keyof T]?: OptionRule<NonNullable<T[K]>>;\n};\n\nexport function createOptionsValidator<T extends object>(\n  defaults: Required<T>,\n  loggerContext: string,\n  rules?: OptionRules<T>,\n): (opts: Partial<T> | undefined) => void {\n  return (opts) => {\n    if (!opts) {\n      return;\n    }\n\n    for (const key of Object.keys(opts)) {\n      if (!(key in defaults)) {\n        continue;\n      }\n\n      const value = opts[key as keyof typeof opts];\n\n      if (value === undefined) {\n        continue;\n      }\n\n      const expected = typeof defaults[key as keyof typeof defaults];\n      const actual = typeof value;\n\n      if (actual !== expected) {\n        throw new Error(\n          `[${loggerContext}] Invalid type for '${key}': expected ${expected}, got ${actual}`,\n        );\n      }\n\n      const rule = rules?.[key as keyof T];\n\n      if (rule) {\n        const msg = (rule.validate as (input: unknown) => string | null)(value);\n\n        if (msg !== null) {\n          throw new Error(`[${loggerContext}] Invalid '${key}': ${msg}`);\n        }\n      }\n    }\n  };\n}\n\n// eslint-disable-next-line no-control-regex -- control characters are exactly what this rule rejects\nconst CONTROL_CHARS = /[\\u0000-\\u001F\\u007F]/;\n\nexport const safeBaseRule: OptionRule<string> = {\n  validate: (value) => {\n    if (CONTROL_CHARS.test(value)) {\n      return \"must not contain control characters\";\n    }\n\n    if (value.split(\"/\").includes(\"..\")) {\n      return \"must not contain '..' segments\";\n    }\n\n    return null;\n  },\n};\n\nexport const safeHashPrefixRule: OptionRule<string> = {\n  validate: (value) => {\n    if (CONTROL_CHARS.test(value)) {\n      return \"must not contain control characters\";\n    }\n\n    if (value.includes(\"/\")) {\n      return \"must not contain '/' (slash is added before the path automatically)\";\n    }\n\n    if (value.includes(\"#\")) {\n      return \"must not contain '#' (it is added as the hash delimiter)\";\n    }\n\n    if (value.includes(\"?\")) {\n      return \"must not contain '?' (it conflicts with the query delimiter)\";\n    }\n\n    return null;\n  },\n};\n\nexport const nonNegativeIntegerRule: OptionRule<number> = {\n  validate: (value) => {\n    if (!Number.isFinite(value)) {\n      return `expected finite number, got ${String(value)}`;\n    }\n\n    if (!Number.isInteger(value)) {\n      return `expected integer, got ${String(value)}`;\n    }\n\n    if (value < 0) {\n      return `expected non-negative integer, got ${value}`;\n    }\n\n    return null;\n  },\n};\n","import { isBrowserEnvironment } from \"./detect.js\";\nimport {\n  pushState,\n  replaceState,\n  addPopstateListener,\n  addHashChangeListener,\n  getHash,\n  getState,\n} from \"./history-api.js\";\nimport {\n  createWarnOnce,\n  createHistoryFallbackBrowser,\n} from \"./ssr-fallback.js\";\n\nimport type { Browser } from \"./types.js\";\n\nexport function createSafeBrowser(\n  getLocation: () => string,\n  context: string,\n): Browser {\n  if (isBrowserEnvironment()) {\n    return {\n      pushState,\n      replaceState,\n      addPopstateListener,\n      addHashChangeListener,\n      getLocation,\n      getHash,\n      getState,\n    };\n  }\n\n  const warnOnce = createWarnOnce(context);\n\n  return {\n    ...createHistoryFallbackBrowser(context),\n    getLocation: () => {\n      warnOnce(\"getLocation\");\n\n      return \"\";\n    },\n  };\n}\n","import { errorCodes, RouterError, UNKNOWN_ROUTE } from \"@real-router/core\";\n\nimport { getRouteFromEvent } from \"./popstate-utils.js\";\n\nimport type { Browser, SharedFactoryState } from \"./types.js\";\nimport type { Params, Plugin, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Navigation options used by the popstate handler to trigger a\n * router.navigate() call from a back/forward event. `source` identifies\n * the origin of the transition to downstream context consumers;\n * `replace: true` keeps the history stack in sync with the browser.\n */\nexport interface PopstateTransitionOptions {\n  source: string;\n  replace: true;\n  forceDeactivate?: boolean;\n}\n\nexport interface PopstateHandlerDeps {\n  router: Router;\n  api: PluginApi;\n  browser: Browser;\n  allowNotFound: boolean;\n  transitionOptions: PopstateTransitionOptions;\n  loggerContext: string;\n  buildUrl: (\n    name: string,\n    params?: Params,\n    options?: { hash?: string },\n  ) => string;\n  /**\n   * Decoded hash of the current browser location (no leading \"#\"). Defaults\n   * to a no-op (returns \"\") for plugins that do not participate in URL\n   * fragment tracking — namely hash-plugin, where `#` is the route delimiter.\n   * (#532)\n   */\n  getCurrentHash?: () => string;\n  /**\n   * Decoded hash from the previous transition's `state.context.url.hash`\n   * (no leading \"#\"). Used by the popstate handler to detect hash-only\n   * navigation and add `force: true, hashChange: true` to bypass SAME_STATES.\n   * Defaults to no-op (returns \"\") for hash-plugin. (#532)\n   */\n  getCurrentContextHash?: () => string;\n}\n\n/**\n * Hash augmentation for popstate-driven navigateToState (#532).\n * Returns a partial options object that the caller spreads on top of\n * `deps.transitionOptions`. When the handler is wired without hash support\n * (hash-plugin), both deps default to undefined and an empty object is\n * returned — preserving the legacy behavior for that plugin.\n */\nfunction resolveHashOptions(\n  deps: PopstateHandlerDeps,\n  matchedPath: string,\n  newHash: string | undefined,\n): { hash?: string; force?: true; hashChange?: true } {\n  // `newHash` is the fragment snapshotted at the event's fire time (#1210), or\n  // `undefined` for plugins without hash support (hash-plugin) — the former\n  // `!deps.getCurrentHash` guard, preserved.\n  if (newHash === undefined) {\n    return {};\n  }\n\n  const prevHash = deps.getCurrentContextHash\n    ? deps.getCurrentContextHash()\n    : \"\";\n  const hashChange =\n    newHash !== prevHash && deps.router.getState()?.path === matchedPath;\n\n  return hashChange\n    ? { hash: newHash, force: true, hashChange: true }\n    : { hash: newHash };\n}\n\nexport function createPopstateHandler(\n  deps: PopstateHandlerDeps,\n): (evt: PopStateEvent | HashChangeEvent) => void {\n  let isTransitioning = false;\n  // Snapshot the route location alongside the event: a deferred popstate is\n  // replayed only after the in-flight navigation's onTransitionSuccess →\n  // replaceState has overwritten the live location, so re-reading it at\n  // replay time would resolve the wrong target (#757).\n  let deferred: {\n    evt: PopStateEvent | HashChangeEvent;\n    location: string;\n    hash: string | undefined;\n  } | null = null;\n\n  function processDeferredEvent(): void {\n    if (deferred) {\n      const { evt, location, hash } = deferred;\n\n      deferred = null;\n      console.warn(\n        `[${deps.loggerContext}] Processing deferred popstate event`,\n      );\n      void onPopState(evt, location, hash);\n    }\n  }\n\n  function rollbackUrlToCurrentState(): void {\n    const currentState = deps.router.getState();\n\n    /* v8 ignore next -- @preserve: router always has state after start(); defensive guard for edge cases */\n    if (!currentState) {\n      return;\n    }\n\n    // Preserve hash on rollback so guard rejection / unmatched URL on\n    // popstate doesn't strip the fragment from the visible URL (#532).\n    const ctxHash = (\n      currentState.context as { url?: { hash?: string } } | undefined\n    )?.url?.hash;\n    const url = deps.buildUrl(\n      currentState.name,\n      currentState.params,\n      ctxHash ? { hash: ctxHash } : undefined,\n    );\n\n    deps.browser.replaceState(currentState, url);\n  }\n\n  function recoverFromCriticalError(error: unknown): void {\n    console.error(\n      `[${deps.loggerContext}] Critical error in onPopState`,\n      error,\n    );\n\n    try {\n      rollbackUrlToCurrentState();\n    } catch (recoveryError) {\n      console.error(\n        `[${deps.loggerContext}] Failed to recover from critical error`,\n        recoveryError,\n      );\n    }\n  }\n\n  async function onPopState(\n    evt: PopStateEvent | HashChangeEvent,\n    location: string,\n    hash: string | undefined,\n  ): Promise<void> {\n    if (isTransitioning) {\n      console.warn(\n        `[${deps.loggerContext}] Transition in progress, deferring popstate event`,\n      );\n      deferred = { evt, location, hash };\n\n      return;\n    }\n\n    isTransitioning = true;\n\n    try {\n      const matched = getRouteFromEvent(evt, deps.api, location);\n\n      if (matched) {\n        // api.navigateToState — plugin-only entry point. Preserves\n        // matchSourceTrailingSlash output and skips the redundant\n        // forwardState/buildPath round-trip (#525). Hash augmentation (#532)\n        // extracted into resolveHashOptions so this branch stays readable.\n        await deps.api.navigateToState(matched, {\n          ...deps.transitionOptions,\n          ...resolveHashOptions(deps, matched.path, hash),\n        });\n      } else if (deps.allowNotFound) {\n        // Same-state short-circuit (#1448): a popstate that resolves to the\n        // UNKNOWN_ROUTE already committed for this exact path is a no-op — skip\n        // the redundant navigateToNotFound so a storm of identical not-found\n        // popstates collapses to a single commit. navigateToNotFound bypasses\n        // the navigate pipeline, so it has no SAME_STATES guard of its own; this\n        // restores parity with the matched-route branch, where a same-state\n        // popstate is already suppressed by navigateToState's SAME_STATES check.\n        const current = deps.router.getState();\n\n        if (current?.name !== UNKNOWN_ROUTE || current.path !== location) {\n          deps.router.navigateToNotFound(location);\n        }\n      } else {\n        // Strict mode — unmatched URL is an error. Emit $$error and sync URL\n        // back to the current router state (no silent fallback to defaultRoute).\n        const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {\n          path: location,\n        });\n\n        deps.api.emitTransitionError(err);\n        rollbackUrlToCurrentState();\n      }\n    } catch (error) {\n      if (error instanceof RouterError) {\n        // navigate() already emitted $$error — just sync URL with router state.\n        // Swallow rollback errors: teardown races may remove router.buildUrl\n        // while a popstate event is still queued.\n        try {\n          rollbackUrlToCurrentState();\n        } catch {\n          // noop — nothing safe to do here\n        }\n      } else {\n        recoverFromCriticalError(error);\n      }\n    } finally {\n      isTransitioning = false;\n      processDeferredEvent();\n    }\n  }\n\n  // Snapshot the location AND fragment the instant the event fires — before any\n  // in-flight navigation can overwrite them via replaceState (#757 for the\n  // path/query location; #1210 for the fragment). A deferred replay must resolve\n  // the target the event referred to, not the live URL a since-committed\n  // in-flight navigation rewrote. `getCurrentHash?.()` is undefined for\n  // hash-less plugins (hash-plugin) — resolveHashOptions treats that as \"no\n  // hash augmentation\", exactly as the old `!deps.getCurrentHash` guard did.\n  return (evt: PopStateEvent | HashChangeEvent) =>\n    void onPopState(evt, deps.browser.getLocation(), deps.getCurrentHash?.());\n}\n\nexport interface PopstateLifecycleDeps {\n  browser: Browser;\n  shared: SharedFactoryState;\n  handler: (evt: PopStateEvent | HashChangeEvent) => void;\n  cleanup: () => void;\n}\n\nexport function createPopstateLifecycle(\n  deps: PopstateLifecycleDeps,\n): Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\"> {\n  // Captured at onStart so onStop/teardown clear the shared slot ONLY while we\n  // still own it. In a factory pool a later router's onStart replaces the slot\n  // (last-wins, #758); clearing it unconditionally on the earlier router's\n  // stop/dispose would disconnect the LIVE router (#1213).\n  let myRemover: (() => void) | undefined;\n\n  return {\n    onStart: () => {\n      if (deps.shared.removePopStateListener) {\n        deps.shared.removePopStateListener();\n      }\n\n      myRemover = deps.browser.addPopstateListener(deps.handler);\n      deps.shared.removePopStateListener = myRemover;\n    },\n\n    onStop: () => {\n      if (myRemover && deps.shared.removePopStateListener === myRemover) {\n        deps.shared.removePopStateListener();\n        deps.shared.removePopStateListener = undefined;\n      }\n\n      myRemover = undefined;\n    },\n\n    teardown: () => {\n      if (myRemover && deps.shared.removePopStateListener === myRemover) {\n        deps.shared.removePopStateListener();\n        deps.shared.removePopStateListener = undefined;\n      }\n\n      myRemover = undefined;\n      deps.cleanup();\n    },\n  };\n}\n\n/**\n * Lifecycle for hash-plugin (#759): syncs the router with the URL fragment on\n * BOTH `popstate` (back/forward) and `hashchange` (external fragment changes —\n * native anchors, address-bar edits, `location.hash = ...`). browser-plugin\n * keeps `createPopstateLifecycle` (popstate only) — for it a path change is a\n * full navigation, and `hashchange` would be noise.\n *\n * Dedup: a hash-changing history traversal fires the `popstate`+`hashchange`\n * pair in ONE browser task — but a microtask checkpoint runs BETWEEN the two\n * listeners (Chromium order: `[popstate, microtask, hashchange, macrotask]`), so\n * the second still lands in the same task. Handling both double-navigates, so the\n * second of the pair is dropped. The two `saw*` flags are **type-scoped** and\n * **order-independent** — whichever of the pair arrives first is handled and\n * blocks the other, no matter which the browser fires first. They reset on a\n * **macrotask** (`setTimeout 0`), which fires AFTER the pair completes (never on\n * the microtask checkpoint mid-pair — that was #1228): the guard spans the whole\n * pair, distinct user gestures land in later macrotasks and are never coalesced,\n * and same-type bursts (two rapid `popstate`s → the deferred-event path) are\n * unaffected because a `popstate` only blocks a following `hashchange`, never\n * another `popstate`.\n *\n * Both listeners are stored under the single `shared.removePopStateListener`\n * slot as a combined remover, preserving the factory-pool last-wins cleanup\n * (#758) unchanged.\n */\nexport function createHashSyncLifecycle(\n  deps: PopstateLifecycleDeps,\n): Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\"> {\n  let sawPopstate = false;\n  let sawHashchange = false;\n  let resetScheduled = false;\n\n  // Reset on a MACROTASK, not a microtask (#1228). The pair fires in one browser\n  // task, but a microtask checkpoint runs BETWEEN the two listeners — verified in\n  // Chromium the order is [popstate, microtask, hashchange, macrotask]. A\n  // queueMicrotask reset therefore cleared the flags before the pair's second\n  // event, which then double-navigated → a phantom SAME_STATES on every hash\n  // back/forward. A setTimeout(0) reset fires AFTER the pair completes (same\n  // task, verified), so the guard spans the whole pair; distinct gestures land in\n  // later macrotasks and are never coalesced.\n  const scheduleReset = (): void => {\n    if (resetScheduled) {\n      return;\n    }\n\n    resetScheduled = true;\n    setTimeout(() => {\n      sawPopstate = false;\n      sawHashchange = false;\n      resetScheduled = false;\n    }, 0);\n  };\n\n  const onPopstate = (evt: PopStateEvent): void => {\n    // The paired hashchange already handled this traversal — drop the popstate.\n    if (sawHashchange) {\n      return;\n    }\n\n    sawPopstate = true;\n    scheduleReset();\n    deps.handler(evt);\n  };\n\n  const onHashchange = (evt: HashChangeEvent): void => {\n    // The paired popstate already handled this traversal — drop the hashchange.\n    if (sawPopstate) {\n      return;\n    }\n\n    sawHashchange = true;\n    scheduleReset();\n    deps.handler(evt);\n  };\n\n  // Captured at onStart so onStop/teardown clear the shared slot ONLY while we\n  // still own the combined remover — a later router's onStart replaces it\n  // (last-wins, #758); clearing it unconditionally on the earlier router's\n  // stop/dispose disconnects the LIVE router (#1213).\n  let myRemover: (() => void) | undefined;\n\n  const removeOwnListeners = (): void => {\n    if (myRemover && deps.shared.removePopStateListener === myRemover) {\n      deps.shared.removePopStateListener();\n      deps.shared.removePopStateListener = undefined;\n    }\n\n    myRemover = undefined;\n  };\n\n  return {\n    onStart: () => {\n      // Unconditional last-wins removal of whatever's installed (#758) — onStart\n      // must displace the previous router regardless of ownership.\n      if (deps.shared.removePopStateListener) {\n        deps.shared.removePopStateListener();\n      }\n\n      const removePopstate = deps.browser.addPopstateListener(onPopstate);\n      const removeHashchange = deps.browser.addHashChangeListener(onHashchange);\n\n      myRemover = () => {\n        removePopstate();\n        removeHashchange();\n      };\n      deps.shared.removePopStateListener = myRemover;\n    },\n\n    onStop: removeOwnListeners,\n\n    teardown: () => {\n      removeOwnListeners();\n      deps.cleanup();\n    },\n  };\n}\n","/**\n * URL fragment (\"hash\") shared layer (#532).\n *\n * Both URL plugins (navigation-plugin, browser-plugin) claim the `\"url\"`\n * `state.context` namespace and write `UrlContext` on every transition.\n * Mutually exclusive at runtime — only one URL plugin is installed per router.\n *\n * Hash form: decoded, no leading \"#\" — symmetric to `params` (no leading \"?\").\n * Encoding to/from URL form happens at the boundary (URL build / URL parse).\n */\n\nexport interface UrlContext {\n  /** Decoded fragment, no leading \"#\". Empty string when URL has no fragment. */\n  hash: string;\n  /** Whether `hash` differs from the previous transition's `state.context.url.hash`. */\n  hashChanged: boolean;\n}\n\n/**\n * Encode for URL fragment per RFC 3986: preserves sub-delims (`&`, `=`, `?`,\n * `:`, etc.) and the path/query characters that `encodeURI` already leaves\n * alone. Defensively percent-escapes `#` (a stray `#` in a decoded fragment\n * would otherwise terminate the fragment in the rendered URL).\n *\n * `encodeURIComponent` over-encodes RFC-3986 sub-delims (`&` → `%26`) and is\n * therefore wrong for fragments.\n */\nexport function encodeHashFragment(decoded: string): string {\n  return encodeURI(decoded).replaceAll(\"#\", \"%23\");\n}\n\n/**\n * Decode a percent-encoded fragment. Falls back to the raw input on malformed\n * escapes — matches the resilience pattern in scroll-restore.\n */\nexport function decodeHashFragment(encoded: string): string {\n  try {\n    return decodeURIComponent(encoded);\n  } catch {\n    return encoded;\n  }\n}\n\n/**\n * Normalize user-provided hash input: strip ALL leading \"#\" characters. The\n * prop is documented to accept the fragment name without \"#\", but we accept a\n * leading \"#\" gracefully. Stripping ALL leading \"#\" (not just one) keeps the\n * function idempotent on pathological inputs like `\"##section\"` (accidental\n * double-hash / concatenation bugs) — `normalize(normalize(x)) === normalize(x)`\n * (property test G9 in `hash-encoding.properties.ts`).\n *\n * **STRICTLY-DECODED contract (#1211 / D1=A).** The input is treated as an\n * already-decoded fragment and is NOT decoded here. A second decode corrupted\n * literal-percent fragments (`\"a%20b\"` → `\"a b\"`, redirect URLs / serialized\n * tokens broken) and split the plugin↔adapter policy: the adapter `<Link>`\n * encoder (`encodeFragmentInline` in `shared/dom-utils/link-utils.ts`) is now\n * strict too, so both layers obey one canonical contract. Callers must pass a\n * DECODED fragment — do NOT pass raw `location.hash` (which is percent-encoded).\n */\nexport function normalizeHashInput(input: string): string {\n  let stripped = input;\n\n  while (stripped.startsWith(\"#\")) {\n    stripped = stripped.slice(1);\n  }\n\n  return stripped;\n}\n\n/**\n * Read the current browser hash in decoded form, no leading \"#\".\n * Accepts any object with a `getHash()` method — works for both `Browser`\n * (History API) and `NavigationBrowser` (Navigation API). SSR-safe via the\n * abstractions, which return `\"\"` outside a real browser.\n */\nexport function getDecodedHash(browser: { getHash: () => string }): string {\n  const raw = browser.getHash();\n\n  if (!raw) {\n    return \"\";\n  }\n\n  const stripped = raw.startsWith(\"#\") ? raw.slice(1) : raw;\n\n  return decodeHashFragment(stripped);\n}\n","export interface ParsedUrl {\n  pathname: string;\n  search: string;\n  hash: string;\n}\n\n/**\n * Scheme-agnostic URL parser.\n *\n * Extracts `pathname`, `search`, and `hash` from any string — absolute\n * (`scheme://authority/path?q#h`), path-relative (`/path?q#h`), or opaque\n * (`data:...`, `javascript:...`). Never throws, never returns null.\n *\n * Routing does not care about scheme or authority, only about the path part.\n * This keeps `browser-plugin`, `navigation-plugin`, and `hash-plugin` working\n * in Electron (`file://`, `app://`), Tauri (`tauri://`, `https://`), and any\n * other webview that may ship with non-HTTP origins. See issue #496.\n */\nconst URL_DELIMITERS: ReadonlySet<string> = new Set([\"/\", \"?\", \"#\"]);\n\nexport function safeParseUrl(url: string): ParsedUrl {\n  let rest = url;\n\n  const schemeIdx = rest.indexOf(\"://\");\n\n  if (schemeIdx !== -1) {\n    const authorityStart = schemeIdx + 3;\n    let pathStart = rest.length;\n\n    for (let i = authorityStart; i < rest.length; i++) {\n      const ch = rest[i];\n\n      if (URL_DELIMITERS.has(ch)) {\n        pathStart = i;\n\n        break;\n      }\n    }\n\n    rest = pathStart === rest.length ? \"/\" : rest.slice(pathStart);\n\n    if (rest.startsWith(\"?\") || rest.startsWith(\"#\")) {\n      rest = `/${rest}`;\n    }\n  }\n\n  const hashIdx = rest.indexOf(\"#\");\n  const hash = hashIdx === -1 ? \"\" : rest.slice(hashIdx);\n  const beforeHash = hashIdx === -1 ? rest : rest.slice(0, hashIdx);\n\n  const queryIdx = beforeHash.indexOf(\"?\");\n  const search = queryIdx === -1 ? \"\" : beforeHash.slice(queryIdx);\n  const pathname = queryIdx === -1 ? beforeHash : beforeHash.slice(0, queryIdx);\n\n  return { pathname, search, hash };\n}\n","import { encodeHashFragment, normalizeHashInput } from \"./url-context.js\";\nimport { buildUrl } from \"./url-utils.js\";\n\nimport type {\n  NavigationOptions,\n  Params,\n  Router,\n  State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport interface LocationSource {\n  getLocation: () => string;\n}\n\n/**\n * Minimal browser surface needed by `createReplaceHistoryState`.\n *\n * Both `Browser` (History API) and navigation-plugin's `NavigationBrowser`\n * (Navigation API) satisfy this structurally — the function never needs\n * `pushState`/`addPopstateListener`, only the replace path.\n */\nexport interface ReplaceStateBrowser {\n  replaceState: (state: unknown, url: string) => void;\n  getHash: () => string;\n}\n\n/**\n * Hash override option for `replaceHistoryState` (#532). Tri-state semantics:\n *   `undefined`  — preserve the current browser hash (legacy behavior, default)\n *   `\"\"`         — explicitly clear the fragment\n *   non-empty    — explicitly set the fragment (decoded form, no leading \"#\")\n */\nexport interface ReplaceHistoryStateOptions {\n  hash?: string;\n}\n\nexport function createStartInterceptor(\n  api: PluginApi,\n  browser: LocationSource,\n): () => void {\n  return api.addInterceptor(\"start\", (next, path) =>\n    next(path ?? browser.getLocation()),\n  );\n}\n\n// Shared `buildUrl` extension for browser-plugin and navigation-plugin.\n// Composes router.buildPath + base prefixing + tri-state hash (#532) into the\n// single function the plugins register via `api.extendRouter({ buildUrl })`.\nexport function createPluginBuildUrl(\n  router: Router,\n  base: string,\n): (route: string, params?: Params, opts?: { hash?: string }) => string {\n  return (route, params, opts) => {\n    const path = router.buildPath(route, params);\n    const url = buildUrl(path, base);\n\n    if (opts?.hash === undefined) {\n      return url;\n    }\n\n    const norm = normalizeHashInput(opts.hash);\n\n    return norm ? `${url}#${encodeHashFragment(norm)}` : url;\n  };\n}\n\nexport function createReplaceHistoryState(\n  api: PluginApi,\n  router: Router,\n  browser: ReplaceStateBrowser,\n  buildUrlFn: (\n    name: string,\n    params?: Params,\n    options?: ReplaceHistoryStateOptions,\n  ) => string,\n  preserveHash = true,\n): (\n  name: string,\n  params?: Params,\n  options?: ReplaceHistoryStateOptions,\n) => void {\n  // Reusable buffer — browsers structured-clone state synchronously inside\n  // replaceState, so the buffer never escapes. Eliminates one allocation per\n  // navigation on the hot path. (Mirrors createUpdateBrowserState.)\n  const buffer = {\n    name: \"\",\n    params: {} as Params,\n    path: \"\",\n  };\n\n  return (\n    name: string,\n    params: Params = {},\n    options?: ReplaceHistoryStateOptions,\n  ) => {\n    const state = api.buildState(name, params);\n\n    if (!state) {\n      throw new Error(\n        `[real-router] Cannot replace state: route \"${name}\" is not found`,\n      );\n    }\n\n    const builtState = api.makeState(\n      state.name,\n      state.params,\n      router.buildPath(state.name, state.params),\n      {\n        params: state.meta,\n      },\n    );\n\n    // Tri-state hash semantics (#532):\n    //   options.hash === undefined → preserve (legacy behavior, controlled by\n    //                                preserveHash flag — true for browser/\n    //                                navigation plugins, false for hash-plugin)\n    //   options.hash === \"\"        → explicitly clear\n    //   options.hash === \"value\"   → explicitly set\n    let hashSegment: string;\n\n    if (options?.hash !== undefined) {\n      const norm = normalizeHashInput(options.hash);\n\n      hashSegment = norm ? `#${encodeHashFragment(norm)}` : \"\";\n    } else if (preserveHash) {\n      hashSegment = browser.getHash();\n    } else {\n      hashSegment = \"\";\n    }\n\n    // The fragment is appended separately as `+ hashSegment`; buildUrlFn is\n    // always called without options. For browser/navigation-plugin hashSegment\n    // carries the explicit or preserved fragment; for hash-plugin it is always\n    // \"\" (preserveHash=false), and the plugin strips { hash } before this runs\n    // (#1230), so no stray fragment is spliced into a hash-route URL.\n    const url = buildUrlFn(name, params) + hashSegment;\n\n    buffer.name = builtState.name;\n    buffer.params = builtState.params;\n    buffer.path = builtState.path;\n\n    browser.replaceState(buffer, url);\n  };\n}\n\nexport function shouldReplaceHistory(\n  navOptions: NavigationOptions,\n  toState: State,\n  fromState: State | undefined,\n): boolean {\n  if (navOptions.replace === true) {\n    return true;\n  }\n\n  if (!fromState) {\n    return navOptions.replace !== false;\n  }\n\n  return !!navOptions.reload && toState.path === fromState.path;\n}\n","// packages/hash-plugin/src/constants.ts\n\nimport type { HashPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<HashPluginOptions> = {\n  hashPrefix: \"\",\n  base: \"\",\n  forceDeactivate: true,\n};\n\n/**\n * Source identifier for transitions triggered by browser events.\n */\nexport const source = \"popstate\";\n\nexport const LOGGER_CONTEXT = \"hash-plugin\";\n","// packages/hash-plugin/src/hash-utils.ts\n\nimport { safelyEncodePath, safeParseUrl } from \"./browser-env\";\n\nfunction escapeRegExp(str: string): string {\n  return str.replaceAll(/[$()*+.?[\\\\\\]^{|}-]/g, String.raw`\\$&`);\n}\n\nexport function createHashPrefixRegex(hashPrefix: string): RegExp | null {\n  if (!hashPrefix) {\n    return null;\n  }\n\n  return new RegExp(`^#${escapeRegExp(hashPrefix)}`);\n}\n\n/**\n * Extract path from URL hash, stripping hash prefix.\n *\n * @param hash - URL hash (e.g., \"#/path\" or \"#!/path\")\n * @param prefixRegex - Pre-compiled regex for prefix stripping (null if no prefix)\n * @returns Extracted path (e.g., \"/path\")\n */\nexport function extractHashPath(\n  hash: string,\n  prefixRegex: RegExp | null,\n): string {\n  if (hash === \"\" || hash === \"#\") {\n    return \"/\";\n  }\n\n  const path = prefixRegex ? hash.replace(prefixRegex, \"\") : hash.slice(1);\n\n  return path || \"/\";\n}\n\nexport function hashUrlToPath(url: string, prefixRegex: RegExp | null): string {\n  const parsedUrl = safeParseUrl(url);\n  const hashPath = extractHashPath(parsedUrl.hash, prefixRegex);\n\n  return hashPath.includes(\"?\") ? hashPath : hashPath + parsedUrl.search;\n}\n\n/**\n * Build the router-side location string from a hash + query pair.\n *\n * Encodes the hash path via `safelyEncodePath` after stripping the\n * configured prefix, then appends the outer `search` only when the hash\n * path itself does not already carry a `?` — otherwise the outer search\n * would be duplicated (see `url.test.ts` — \"well-formed path (no double '?')\").\n *\n * Used by the `createSafeBrowser` `getLocation` callback both in the\n * production factory and in functional/stress test helpers. Extracting\n * here keeps the production path and test mocks aligned; a regression in\n * this logic previously slipped between the two.\n */\nexport function buildHashLocation(\n  hash: string,\n  search: string,\n  prefixRegex: RegExp | null,\n): string {\n  const hashPath = safelyEncodePath(extractHashPath(hash, prefixRegex));\n\n  return hashPath.includes(\"?\") ? hashPath : hashPath + search;\n}\n","import {\n  canSkipPopstateHistoryWrite,\n  createPopstateHandler,\n  createHashSyncLifecycle,\n  createStartInterceptor,\n  createReplaceHistoryState,\n  shouldReplaceHistory,\n  updateBrowserState,\n} from \"./browser-env\";\nimport { LOGGER_CONTEXT, source as POPSTATE_SOURCE } from \"./constants\";\nimport { hashUrlToPath } from \"./hash-utils\";\n\nimport type { Browser, SharedFactoryState } from \"./browser-env\";\nimport type { HashPluginOptions } from \"./types\";\nimport type {\n  NavigationOptions,\n  Params,\n  Router,\n  State,\n  Plugin,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport class HashPlugin {\n  readonly #router: Router;\n  readonly #browser: Browser;\n  readonly #urlPrefix: string;\n  readonly #removeStartInterceptor: () => void;\n  readonly #removeExtensions: () => void;\n  readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n  readonly #warnHashIgnored!: () => void;\n\n  constructor(\n    router: Router,\n    api: PluginApi,\n    options: Required<HashPluginOptions>,\n    browser: Browser,\n    prefixRegex: RegExp | null,\n    transitionOptions: {\n      source: string;\n      replace: true;\n      forceDeactivate?: boolean;\n    },\n    shared: SharedFactoryState,\n  ) {\n    this.#router = router;\n    this.#browser = browser;\n\n    this.#removeStartInterceptor = createStartInterceptor(api, browser);\n\n    // Hash limitation warn-once (#532). hash-plugin uses `#` as the route\n    // delimiter, so URL fragments are structurally incompatible. Plugin\n    // accepts the `hash` option for typing parity with browser/navigation\n    // plugins, ignores it, and emits a single console.warn the first time\n    // any consumer surfaces a hash. Existing `createWarnOnce` in browser-env\n    // is SSR-specific (different signature) — inline pattern here.\n    let hashWarned = false;\n    const warnHashIgnored = (): void => {\n      if (hashWarned) {\n        return;\n      }\n\n      hashWarned = true;\n      console.warn(\n        \"[@real-router/hash-plugin] `hash` option is ignored — `#` is reserved for the route delimiter. \" +\n          \"URL fragments are not supported with hash-plugin; use @real-router/browser-plugin or \" +\n          \"@real-router/navigation-plugin if you need them.\",\n      );\n    };\n\n    this.#urlPrefix = `${options.base}#${options.hashPrefix}`;\n    const pluginBuildUrl = (\n      route: string,\n      params?: Params,\n      opts?: { hash?: string },\n    ) => {\n      if (opts?.hash !== undefined) {\n        warnHashIgnored();\n      }\n\n      return this.#urlPrefix + router.buildPath(route, params);\n    };\n\n    this.#warnHashIgnored = warnHashIgnored;\n\n    const replaceHistoryStateImpl = createReplaceHistoryState(\n      api,\n      router,\n      browser,\n      pluginBuildUrl,\n      false,\n    );\n\n    this.#removeExtensions = api.extendRouter({\n      buildUrl: pluginBuildUrl,\n      matchUrl: (url: string) =>\n        api.matchPath(hashUrlToPath(url, prefixRegex)) ?? undefined,\n      // #532/#1230: hash-plugin ignores URL fragments (`#` is the route\n      // delimiter). Warn once and drop `{ hash }` — mirroring buildUrl/navigate.\n      // Without this, createReplaceHistoryState's explicit-hash branch splices\n      // \"#x\" into the hash-route URL regardless of preserveHash=false.\n      replaceHistoryState: (\n        name: string,\n        params?: Params,\n        opts?: { hash?: string },\n      ) => {\n        if (opts?.hash !== undefined) {\n          warnHashIgnored();\n        }\n\n        replaceHistoryStateImpl(name, params);\n      },\n    });\n\n    const handler = createPopstateHandler({\n      router,\n      api,\n      browser,\n      allowNotFound: api.getOptions().allowNotFound,\n      transitionOptions,\n      loggerContext: LOGGER_CONTEXT,\n      buildUrl: pluginBuildUrl,\n    });\n\n    this.#lifecycle = createHashSyncLifecycle({\n      browser,\n      shared,\n      handler,\n      cleanup: () => {\n        this.#removeStartInterceptor();\n        this.#removeExtensions();\n      },\n    });\n  }\n\n  getPlugin(): Plugin {\n    return {\n      ...this.#lifecycle,\n\n      onTransitionSuccess: (\n        toState: State,\n        fromState: State | undefined,\n        navOptions: NavigationOptions,\n      ) => {\n        // Hash limitation (#532): warn once if a consumer programmatically\n        // requested a fragment via `router.navigate(..., { hash })`.\n        if (navOptions.hash !== undefined) {\n          this.#warnHashIgnored();\n        }\n\n        const replaceHistory = shouldReplaceHistory(\n          navOptions,\n          toState,\n          fromState,\n        );\n\n        const isPopstate = navOptions.source === POPSTATE_SOURCE;\n\n        // On back/forward the browser has already restored the target entry's\n        // {name,params,path} + URL, so hash-plugin's replaceState re-writes the\n        // same values — a value-level no-op that still fires a second\n        // updateForSameDocumentNavigation Blink event. Skip it when provably a\n        // no-op; every load-bearing case (redirect, normalization, corrupted\n        // history.state) keeps the write. (#1353)\n        const skipHistoryWrite =\n          isPopstate &&\n          replaceHistory &&\n          canSkipPopstateHistoryWrite(\n            toState,\n            this.#browser,\n            this.#router.areStatesEqual,\n          );\n\n        if (!skipHistoryWrite) {\n          // Build from toState.path, not buildUrl(name): for UNKNOWN_ROUTE\n          // buildPath(name) is \"\" and the typed URL would collapse to the bare\n          // prefix. toState.path is already final and, for matched routes,\n          // equals buildPath(name, params) — so matched behavior is identical\n          // and the 404's typed path is preserved. (#1229)\n          const url = this.#urlPrefix + toState.path;\n\n          updateBrowserState(toState, url, replaceHistory, this.#browser);\n        }\n      },\n    };\n  }\n}\n","import {\n  createOptionsValidator,\n  safeBaseRule,\n  safeHashPrefixRule,\n} from \"./browser-env\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { HashPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<HashPluginOptions>(\n  defaultOptions,\n  LOGGER_CONTEXT,\n  { base: safeBaseRule, hashPrefix: safeHashPrefixRule },\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { createSafeBrowser, normalizeBase } from \"./browser-env\";\nimport { defaultOptions, source } from \"./constants\";\nimport { buildHashLocation, createHashPrefixRegex } from \"./hash-utils\";\nimport { HashPlugin } from \"./plugin\";\nimport { validateOptions } from \"./validation\";\n\nimport type { Browser, SharedFactoryState } from \"./browser-env\";\nimport type { HashPluginOptions } from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function hashPluginFactory(\n  opts?: Partial<HashPluginOptions>,\n  browser?: Browser,\n): PluginFactory {\n  validateOptions(opts);\n\n  const definedOpts = opts\n    ? Object.fromEntries(\n        Object.entries(opts).filter(\n          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime may receive explicit undefined via conditional spreads (exactOptionalPropertyTypes does not apply here)\n          ([, value]) => value !== undefined,\n        ),\n      )\n    : {};\n  const options: Required<HashPluginOptions> = {\n    ...defaultOptions,\n    ...definedOpts,\n  };\n\n  options.base = normalizeBase(options.base);\n\n  const prefixRegex = createHashPrefixRegex(options.hashPrefix);\n  const resolvedBrowser =\n    browser ??\n    createSafeBrowser(\n      () =>\n        buildHashLocation(\n          globalThis.location.hash,\n          globalThis.location.search,\n          prefixRegex,\n        ),\n      \"hash-plugin\",\n    );\n\n  const transitionOptions = {\n    forceDeactivate: options.forceDeactivate,\n    source,\n    replace: true as const,\n  };\n\n  const shared: SharedFactoryState = { removePopStateListener: undefined };\n\n  return function hashPlugin(routerBase) {\n    const plugin = new HashPlugin(\n      routerBase as Router,\n      getPluginApi(routerBase),\n      options,\n      resolvedBrowser,\n      prefixRegex,\n      transitionOptions,\n      shared,\n    );\n\n    return plugin.getPlugin();\n  };\n}\n"],"mappings":"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"}