UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

1 lines 19.2 kB
{"version":3,"file":"ssr-server.cjs","names":[],"sources":["../../../src/ssr/ssr-server.ts"],"sourcesContent":["import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'\nimport invariant from 'tiny-invariant'\nimport { decodePath } from '../utils'\nimport { createLRUCache } from '../lru-cache'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'\nimport { dehydrateSsrMatchId } from './ssr-match-id'\nimport { defaultSerovalPlugins } from './serializer/seroval-plugins'\nimport { makeSsrSerovalPlugin } from './serializer/transformer'\nimport type { LRUCache } from '../lru-cache'\nimport type { DehydratedMatch, DehydratedRouter } from './types'\nimport type { AnySerializationAdapter } from './serializer/transformer'\nimport type { AnyRouter } from '../router'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest, RouterManagedTag } from '../manifest'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n cleanup: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n }\n onSerializationFinished: {\n type: 'onSerializationFinished'\n }\n }\n}\n\nconst SCOPE_ID = 'tsr'\n\nconst TSR_PREFIX = GLOBAL_TSR + '.router='\nconst P_PREFIX = GLOBAL_TSR + '.p(()=>'\nconst P_SUFFIX = ')'\n\nexport function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {\n const dehydratedMatch: DehydratedMatch = {\n i: dehydrateSsrMatchId(match.id),\n u: match.updatedAt,\n s: match.status,\n }\n\n const properties = [\n ['__beforeLoadContext', 'b'],\n ['loaderData', 'l'],\n ['error', 'e'],\n ['ssr', 'ssr'],\n ] as const\n\n for (const [key, shorthand] of properties) {\n if (match[key] !== undefined) {\n dehydratedMatch[shorthand] = match[key]\n }\n }\n if (match.globalNotFound) {\n dehydratedMatch.g = true\n }\n return dehydratedMatch\n}\n\nconst INITIAL_SCRIPTS = [\n getCrossReferenceHeader(SCOPE_ID),\n minifiedTsrBootStrapScript,\n]\n\nclass ScriptBuffer {\n private router: AnyRouter | undefined\n private _queue: Array<string>\n private _scriptBarrierLifted = false\n private _cleanedUp = false\n private _pendingMicrotask = false\n\n constructor(router: AnyRouter) {\n this.router = router\n // Copy INITIAL_SCRIPTS to avoid mutating the shared array\n this._queue = INITIAL_SCRIPTS.slice()\n }\n\n enqueue(script: string) {\n if (this._cleanedUp) return\n this._queue.push(script)\n // If barrier is lifted, schedule injection (if not already scheduled)\n if (this._scriptBarrierLifted && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n liftBarrier() {\n if (this._scriptBarrierLifted || this._cleanedUp) return\n this._scriptBarrierLifted = true\n if (this._queue.length > 0 && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n /**\n * Flushes any pending scripts synchronously.\n * Call this before emitting onSerializationFinished to ensure all scripts are injected.\n *\n * IMPORTANT: Only injects if the barrier has been lifted. Before the barrier is lifted,\n * scripts should remain in the queue so takeBufferedScripts() can retrieve them\n */\n flush() {\n if (!this._scriptBarrierLifted) return\n if (this._cleanedUp) return\n this._pendingMicrotask = false\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n takeAll() {\n const bufferedScripts = this._queue\n this._queue = []\n if (bufferedScripts.length === 0) {\n return undefined\n }\n // Optimization: if only one script, avoid join\n if (bufferedScripts.length === 1) {\n return bufferedScripts[0] + ';document.currentScript.remove()'\n }\n // Append cleanup script and join - avoid push() to not mutate then iterate\n return bufferedScripts.join(';') + ';document.currentScript.remove()'\n }\n\n injectBufferedScripts() {\n if (this._cleanedUp) return\n // Early return if queue is empty (avoids unnecessary takeAll() call)\n if (this._queue.length === 0) return\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n cleanup() {\n this._cleanedUp = true\n this._queue = []\n this.router = undefined\n }\n}\n\nconst isProd = process.env.NODE_ENV === 'production'\n\ntype FilteredRoutes = Manifest['routes']\n\ntype ManifestLRU = LRUCache<string, FilteredRoutes>\n\nconst MANIFEST_CACHE_SIZE = 100\nconst manifestCaches = new WeakMap<Manifest, ManifestLRU>()\n\nfunction getManifestCache(manifest: Manifest): ManifestLRU {\n const cache = manifestCaches.get(manifest)\n if (cache) return cache\n const newCache = createLRUCache<string, FilteredRoutes>(MANIFEST_CACHE_SIZE)\n manifestCaches.set(manifest, newCache)\n return newCache\n}\n\nexport function attachRouterServerSsrUtils({\n router,\n manifest,\n}: {\n router: AnyRouter\n manifest: Manifest | undefined\n}) {\n router.ssr = {\n manifest,\n }\n let _dehydrated = false\n let _serializationFinished = false\n const renderFinishedListeners: Array<() => void> = []\n const serializationFinishedListeners: Array<() => void> = []\n const scriptBuffer = new ScriptBuffer(router)\n let injectedHtmlBuffer = ''\n\n router.serverSsr = {\n injectHtml: (html: string) => {\n if (!html) return\n // Buffer the HTML so it can be retrieved via takeBufferedHtml()\n injectedHtmlBuffer += html\n // Emit event to notify subscribers that new HTML is available\n router.emit({\n type: 'onInjectedHtml',\n })\n },\n injectScript: (script: string) => {\n if (!script) return\n const html = `<script${router.options.ssr?.nonce ? ` nonce='${router.options.ssr.nonce}'` : ''}>${script}</script>`\n router.serverSsr!.injectHtml(html)\n },\n dehydrate: async () => {\n invariant(!_dehydrated, 'router is already dehydrated!')\n let matchesToDehydrate = router.state.matches\n if (router.isShell()) {\n // In SPA mode we only want to dehydrate the root match\n matchesToDehydrate = matchesToDehydrate.slice(0, 1)\n }\n const matches = matchesToDehydrate.map(dehydrateMatch)\n\n let manifestToDehydrate: Manifest | undefined = undefined\n // For currently matched routes, send full manifest (preloads + assets)\n // For all other routes, only send assets (no preloads as they are handled via dynamic imports)\n if (manifest) {\n // Prod-only caching; in dev manifests may be replaced/updated (HMR)\n const currentRouteIdsList = matchesToDehydrate.map((m) => m.routeId)\n const manifestCacheKey = currentRouteIdsList.join('\\0')\n\n let filteredRoutes: FilteredRoutes | undefined\n\n if (isProd) {\n filteredRoutes = getManifestCache(manifest).get(manifestCacheKey)\n }\n\n if (!filteredRoutes) {\n const currentRouteIds = new Set(currentRouteIdsList)\n const nextFilteredRoutes: FilteredRoutes = {}\n\n for (const routeId in manifest.routes) {\n const routeManifest = manifest.routes[routeId]!\n if (currentRouteIds.has(routeId)) {\n nextFilteredRoutes[routeId] = routeManifest\n } else if (\n routeManifest.assets &&\n routeManifest.assets.length > 0\n ) {\n nextFilteredRoutes[routeId] = {\n assets: routeManifest.assets,\n }\n }\n }\n\n if (isProd) {\n getManifestCache(manifest).set(manifestCacheKey, nextFilteredRoutes)\n }\n\n filteredRoutes = nextFilteredRoutes\n }\n\n manifestToDehydrate = {\n routes: filteredRoutes,\n }\n }\n const dehydratedRouter: DehydratedRouter = {\n manifest: manifestToDehydrate,\n matches,\n }\n const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id\n if (lastMatchId) {\n dehydratedRouter.lastMatchId = dehydrateSsrMatchId(lastMatchId)\n }\n const dehydratedData = await router.options.dehydrate?.()\n if (dehydratedData) {\n dehydratedRouter.dehydratedData = dehydratedData\n }\n _dehydrated = true\n\n const trackPlugins = { didRun: false }\n const serializationAdapters = router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n const plugins = serializationAdapters\n ? serializationAdapters\n .map((t) => makeSsrSerovalPlugin(t, trackPlugins))\n .concat(defaultSerovalPlugins)\n : defaultSerovalPlugins\n\n const signalSerializationComplete = () => {\n _serializationFinished = true\n try {\n serializationFinishedListeners.forEach((l) => l())\n router.emit({ type: 'onSerializationFinished' })\n } catch (err) {\n console.error('Serialization listener error:', err)\n } finally {\n serializationFinishedListeners.length = 0\n renderFinishedListeners.length = 0\n }\n }\n\n crossSerializeStream(dehydratedRouter, {\n refs: new Map(),\n plugins,\n onSerialize: (data, initial) => {\n let serialized = initial ? TSR_PREFIX + data : data\n if (trackPlugins.didRun) {\n serialized = P_PREFIX + serialized + P_SUFFIX\n }\n scriptBuffer.enqueue(serialized)\n },\n scopeId: SCOPE_ID,\n onDone: () => {\n scriptBuffer.enqueue(GLOBAL_TSR + '.e()')\n // Flush all pending scripts synchronously before signaling completion\n // This ensures all scripts are injected before onSerializationFinished is emitted\n scriptBuffer.flush()\n signalSerializationComplete()\n },\n onError: (err) => {\n console.error('Serialization error:', err)\n signalSerializationComplete()\n },\n })\n },\n isDehydrated() {\n return _dehydrated\n },\n isSerializationFinished() {\n return _serializationFinished\n },\n onRenderFinished: (listener) => renderFinishedListeners.push(listener),\n onSerializationFinished: (listener) =>\n serializationFinishedListeners.push(listener),\n setRenderFinished: () => {\n // Wrap in try-catch to ensure scriptBuffer.liftBarrier() is always called\n try {\n renderFinishedListeners.forEach((l) => l())\n } catch (err) {\n console.error('Error in render finished listener:', err)\n } finally {\n // Clear listeners after calling them to prevent memory leaks\n renderFinishedListeners.length = 0\n }\n scriptBuffer.liftBarrier()\n },\n takeBufferedScripts() {\n const scripts = scriptBuffer.takeAll()\n const serverBufferedScript: RouterManagedTag = {\n tag: 'script',\n attrs: {\n nonce: router.options.ssr?.nonce,\n className: '$tsr',\n id: TSR_SCRIPT_BARRIER_ID,\n },\n children: scripts,\n }\n return serverBufferedScript\n },\n liftScriptBarrier() {\n scriptBuffer.liftBarrier()\n },\n takeBufferedHtml() {\n if (!injectedHtmlBuffer) {\n return undefined\n }\n const buffered = injectedHtmlBuffer\n injectedHtmlBuffer = ''\n return buffered\n },\n cleanup() {\n // Guard against multiple cleanup calls\n if (!router.serverSsr) return\n renderFinishedListeners.length = 0\n serializationFinishedListeners.length = 0\n injectedHtmlBuffer = ''\n scriptBuffer.cleanup()\n router.serverSsr = undefined\n },\n }\n}\n\n/**\n * Get the origin for the request.\n *\n * SECURITY: We intentionally do NOT trust the Origin header for determining\n * the router's origin. The Origin header can be spoofed by attackers, which\n * could lead to SSRF-like vulnerabilities where redirects are constructed\n * using a malicious origin (CVE-2024-34351).\n *\n * Instead, we derive the origin from request.url, which is typically set by\n * the server infrastructure (not client-controlled headers).\n *\n * For applications behind proxies that need to trust forwarded headers,\n * use the router's `origin` option to explicitly configure a trusted origin.\n */\nexport function getOrigin(request: Request) {\n try {\n return new URL(request.url).origin\n } catch {}\n return 'http://localhost'\n}\n\n// server and browser can decode/encode characters differently in paths and search params.\n// Server generally strictly follows the WHATWG URL Standard, while browsers may differ for legacy reasons.\n// for example, in paths \"|\" is not encoded on the server but is encoded on chromium (and not on firefox) while \"\" is encoded on both sides.\n// Another anomaly is that in Node new URLSearchParams and new URL also decode/encode characters differently.\n// new URLSearchParams() encodes \"|\" while new URL() does not, and in this instance\n// chromium treats search params differently than paths, i.e. \"|\" is not encoded in search params.\nexport function getNormalizedURL(url: string | URL, base?: string | URL) {\n // ensure backslashes are encoded correctly in the URL\n if (typeof url === 'string') url = url.replace('\\\\', '%5C')\n\n const rawUrl = new URL(url, base)\n const { path: decodedPathname, handledProtocolRelativeURL } = decodePath(\n rawUrl.pathname,\n )\n const searchParams = new URLSearchParams(rawUrl.search)\n const normalizedHref =\n decodedPathname +\n (searchParams.size > 0 ? '?' : '') +\n searchParams.toString() +\n rawUrl.hash\n\n return {\n url: new URL(normalizedHref, rawUrl.origin),\n handledProtocolRelativeURL,\n }\n}\n"],"mappings":";;;;;;;;;;;;AA+BA,IAAM,WAAW;AAEjB,IAAM,aAAa,kBAAA,aAAa;AAChC,IAAM,WAAW,kBAAA,aAAa;AAC9B,IAAM,WAAW;AAEjB,SAAgB,eAAe,OAAuC;CACpE,MAAM,kBAAmC;EACvC,GAAG,qBAAA,oBAAoB,MAAM,GAAG;EAChC,GAAG,MAAM;EACT,GAAG,MAAM;EACV;AASD,MAAK,MAAM,CAAC,KAAK,cAPE;EACjB,CAAC,uBAAuB,IAAI;EAC5B,CAAC,cAAc,IAAI;EACnB,CAAC,SAAS,IAAI;EACd,CAAC,OAAO,MAAM;EACf,CAGC,KAAI,MAAM,SAAS,KAAA,EACjB,iBAAgB,aAAa,MAAM;AAGvC,KAAI,MAAM,eACR,iBAAgB,IAAI;AAEtB,QAAO;;AAGT,IAAM,kBAAkB,EAAA,GAAA,QAAA,yBACE,SAAS,EACjC,kBAAA,QACD;AAED,IAAM,eAAN,MAAmB;CAOjB,YAAY,QAAmB;8BAJA;oBACV;2BACO;AAG1B,OAAK,SAAS;AAEd,OAAK,SAAS,gBAAgB,OAAO;;CAGvC,QAAQ,QAAgB;AACtB,MAAI,KAAK,WAAY;AACrB,OAAK,OAAO,KAAK,OAAO;AAExB,MAAI,KAAK,wBAAwB,CAAC,KAAK,mBAAmB;AACxD,QAAK,oBAAoB;AACzB,wBAAqB;AACnB,SAAK,oBAAoB;AACzB,SAAK,uBAAuB;KAC5B;;;CAIN,cAAc;AACZ,MAAI,KAAK,wBAAwB,KAAK,WAAY;AAClD,OAAK,uBAAuB;AAC5B,MAAI,KAAK,OAAO,SAAS,KAAK,CAAC,KAAK,mBAAmB;AACrD,QAAK,oBAAoB;AACzB,wBAAqB;AACnB,SAAK,oBAAoB;AACzB,SAAK,uBAAuB;KAC5B;;;;;;;;;;CAWN,QAAQ;AACN,MAAI,CAAC,KAAK,qBAAsB;AAChC,MAAI,KAAK,WAAY;AACrB,OAAK,oBAAoB;EACzB,MAAM,kBAAkB,KAAK,SAAS;AACtC,MAAI,mBAAmB,KAAK,QAAQ,UAClC,MAAK,OAAO,UAAU,aAAa,gBAAgB;;CAIvD,UAAU;EACR,MAAM,kBAAkB,KAAK;AAC7B,OAAK,SAAS,EAAE;AAChB,MAAI,gBAAgB,WAAW,EAC7B;AAGF,MAAI,gBAAgB,WAAW,EAC7B,QAAO,gBAAgB,KAAK;AAG9B,SAAO,gBAAgB,KAAK,IAAI,GAAG;;CAGrC,wBAAwB;AACtB,MAAI,KAAK,WAAY;AAErB,MAAI,KAAK,OAAO,WAAW,EAAG;EAC9B,MAAM,kBAAkB,KAAK,SAAS;AACtC,MAAI,mBAAmB,KAAK,QAAQ,UAClC,MAAK,OAAO,UAAU,aAAa,gBAAgB;;CAIvD,UAAU;AACR,OAAK,aAAa;AAClB,OAAK,SAAS,EAAE;AAChB,OAAK,SAAS,KAAA;;;AAIlB,IAAM,SAAA,QAAA,IAAA,aAAkC;AAMxC,IAAM,sBAAsB;AAC5B,IAAM,iCAAiB,IAAI,SAAgC;AAE3D,SAAS,iBAAiB,UAAiC;CACzD,MAAM,QAAQ,eAAe,IAAI,SAAS;AAC1C,KAAI,MAAO,QAAO;CAClB,MAAM,WAAW,kBAAA,eAAuC,oBAAoB;AAC5E,gBAAe,IAAI,UAAU,SAAS;AACtC,QAAO;;AAGT,SAAgB,2BAA2B,EACzC,QACA,YAIC;AACD,QAAO,MAAM,EACX,UACD;CACD,IAAI,cAAc;CAClB,IAAI,yBAAyB;CAC7B,MAAM,0BAA6C,EAAE;CACrD,MAAM,iCAAoD,EAAE;CAC5D,MAAM,eAAe,IAAI,aAAa,OAAO;CAC7C,IAAI,qBAAqB;AAEzB,QAAO,YAAY;EACjB,aAAa,SAAiB;AAC5B,OAAI,CAAC,KAAM;AAEX,yBAAsB;AAEtB,UAAO,KAAK,EACV,MAAM,kBACP,CAAC;;EAEJ,eAAe,WAAmB;AAChC,OAAI,CAAC,OAAQ;GACb,MAAM,OAAO,UAAU,OAAO,QAAQ,KAAK,QAAQ,WAAW,OAAO,QAAQ,IAAI,MAAM,KAAK,GAAG,GAAG,OAAO;AACzG,UAAO,UAAW,WAAW,KAAK;;EAEpC,WAAW,YAAY;AACrB,IAAA,GAAA,eAAA,SAAU,CAAC,aAAa,gCAAgC;GACxD,IAAI,qBAAqB,OAAO,MAAM;AACtC,OAAI,OAAO,SAAS,CAElB,sBAAqB,mBAAmB,MAAM,GAAG,EAAE;GAErD,MAAM,UAAU,mBAAmB,IAAI,eAAe;GAEtD,IAAI,sBAA4C,KAAA;AAGhD,OAAI,UAAU;IAEZ,MAAM,sBAAsB,mBAAmB,KAAK,MAAM,EAAE,QAAQ;IACpE,MAAM,mBAAmB,oBAAoB,KAAK,KAAK;IAEvD,IAAI;AAEJ,QAAI,OACF,kBAAiB,iBAAiB,SAAS,CAAC,IAAI,iBAAiB;AAGnE,QAAI,CAAC,gBAAgB;KACnB,MAAM,kBAAkB,IAAI,IAAI,oBAAoB;KACpD,MAAM,qBAAqC,EAAE;AAE7C,UAAK,MAAM,WAAW,SAAS,QAAQ;MACrC,MAAM,gBAAgB,SAAS,OAAO;AACtC,UAAI,gBAAgB,IAAI,QAAQ,CAC9B,oBAAmB,WAAW;eAE9B,cAAc,UACd,cAAc,OAAO,SAAS,EAE9B,oBAAmB,WAAW,EAC5B,QAAQ,cAAc,QACvB;;AAIL,SAAI,OACF,kBAAiB,SAAS,CAAC,IAAI,kBAAkB,mBAAmB;AAGtE,sBAAiB;;AAGnB,0BAAsB,EACpB,QAAQ,gBACT;;GAEH,MAAM,mBAAqC;IACzC,UAAU;IACV;IACD;GACD,MAAM,cAAc,mBAAmB,mBAAmB,SAAS,IAAI;AACvE,OAAI,YACF,kBAAiB,cAAc,qBAAA,oBAAoB,YAAY;GAEjE,MAAM,iBAAiB,MAAM,OAAO,QAAQ,aAAa;AACzD,OAAI,eACF,kBAAiB,iBAAiB;AAEpC,iBAAc;GAEd,MAAM,eAAe,EAAE,QAAQ,OAAO;GACtC,MAAM,wBAAwB,OAAO,QAAQ;GAG7C,MAAM,UAAU,wBACZ,sBACG,KAAK,MAAM,oBAAA,qBAAqB,GAAG,aAAa,CAAC,CACjD,OAAO,wBAAA,sBAAsB,GAChC,wBAAA;GAEJ,MAAM,oCAAoC;AACxC,6BAAyB;AACzB,QAAI;AACF,oCAA+B,SAAS,MAAM,GAAG,CAAC;AAClD,YAAO,KAAK,EAAE,MAAM,2BAA2B,CAAC;aACzC,KAAK;AACZ,aAAQ,MAAM,iCAAiC,IAAI;cAC3C;AACR,oCAA+B,SAAS;AACxC,6BAAwB,SAAS;;;AAIrC,IAAA,GAAA,QAAA,sBAAqB,kBAAkB;IACrC,sBAAM,IAAI,KAAK;IACf;IACA,cAAc,MAAM,YAAY;KAC9B,IAAI,aAAa,UAAU,aAAa,OAAO;AAC/C,SAAI,aAAa,OACf,cAAa,WAAW,aAAa;AAEvC,kBAAa,QAAQ,WAAW;;IAElC,SAAS;IACT,cAAc;AACZ,kBAAa,QAAQ,kBAAA,aAAa,OAAO;AAGzC,kBAAa,OAAO;AACpB,kCAA6B;;IAE/B,UAAU,QAAQ;AAChB,aAAQ,MAAM,wBAAwB,IAAI;AAC1C,kCAA6B;;IAEhC,CAAC;;EAEJ,eAAe;AACb,UAAO;;EAET,0BAA0B;AACxB,UAAO;;EAET,mBAAmB,aAAa,wBAAwB,KAAK,SAAS;EACtE,0BAA0B,aACxB,+BAA+B,KAAK,SAAS;EAC/C,yBAAyB;AAEvB,OAAI;AACF,4BAAwB,SAAS,MAAM,GAAG,CAAC;YACpC,KAAK;AACZ,YAAQ,MAAM,sCAAsC,IAAI;aAChD;AAER,4BAAwB,SAAS;;AAEnC,gBAAa,aAAa;;EAE5B,sBAAsB;GACpB,MAAM,UAAU,aAAa,SAAS;AAUtC,UAT+C;IAC7C,KAAK;IACL,OAAO;KACL,OAAO,OAAO,QAAQ,KAAK;KAC3B,WAAW;KACX,IAAI,kBAAA;KACL;IACD,UAAU;IACX;;EAGH,oBAAoB;AAClB,gBAAa,aAAa;;EAE5B,mBAAmB;AACjB,OAAI,CAAC,mBACH;GAEF,MAAM,WAAW;AACjB,wBAAqB;AACrB,UAAO;;EAET,UAAU;AAER,OAAI,CAAC,OAAO,UAAW;AACvB,2BAAwB,SAAS;AACjC,kCAA+B,SAAS;AACxC,wBAAqB;AACrB,gBAAa,SAAS;AACtB,UAAO,YAAY,KAAA;;EAEtB;;;;;;;;;;;;;;;;AAiBH,SAAgB,UAAU,SAAkB;AAC1C,KAAI;AACF,SAAO,IAAI,IAAI,QAAQ,IAAI,CAAC;SACtB;AACR,QAAO;;AAST,SAAgB,iBAAiB,KAAmB,MAAqB;AAEvE,KAAI,OAAO,QAAQ,SAAU,OAAM,IAAI,QAAQ,MAAM,MAAM;CAE3D,MAAM,SAAS,IAAI,IAAI,KAAK,KAAK;CACjC,MAAM,EAAE,MAAM,iBAAiB,+BAA+B,cAAA,WAC5D,OAAO,SACR;CACD,MAAM,eAAe,IAAI,gBAAgB,OAAO,OAAO;CACvD,MAAM,iBACJ,mBACC,aAAa,OAAO,IAAI,MAAM,MAC/B,aAAa,UAAU,GACvB,OAAO;AAET,QAAO;EACL,KAAK,IAAI,IAAI,gBAAgB,OAAO,OAAO;EAC3C;EACD"}