@tanstack/router-core 1.157.4 → 1.157.6
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.
- package/dist/cjs/load-matches.cjs +4 -4
- package/dist/cjs/load-matches.cjs.map +1 -1
- package/dist/cjs/router.cjs +29 -13
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +0 -9
- package/dist/cjs/ssr/ssr-client.cjs +2 -2
- package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.cjs +48 -29
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +138 -42
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
- package/dist/cjs/utils/batch.cjs +16 -0
- package/dist/cjs/utils/batch.cjs.map +1 -0
- package/dist/cjs/utils/batch.d.cts +1 -0
- package/dist/cjs/utils.cjs +4 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/load-matches.js +1 -1
- package/dist/esm/load-matches.js.map +1 -1
- package/dist/esm/router.d.ts +0 -9
- package/dist/esm/router.js +28 -12
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/ssr/ssr-client.js +1 -1
- package/dist/esm/ssr/ssr-client.js.map +1 -1
- package/dist/esm/ssr/ssr-server.js +48 -29
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.js +138 -42
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
- package/dist/esm/utils/batch.d.ts +1 -0
- package/dist/esm/utils/batch.js +16 -0
- package/dist/esm/utils/batch.js.map +1 -0
- package/dist/esm/utils.js +4 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/load-matches.ts +1 -1
- package/src/router.ts +39 -14
- package/src/ssr/ssr-client.ts +1 -1
- package/src/ssr/ssr-server.ts +69 -32
- package/src/ssr/transformStreamWithRouter.ts +187 -108
- package/src/utils/batch.ts +18 -0
- package/src/utils.ts +4 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transformStreamWithRouter.js","sources":["../../../src/ssr/transformStreamWithRouter.ts"],"sourcesContent":["import { ReadableStream } from 'node:stream/web'\nimport { Readable } from 'node:stream'\nimport { TSR_SCRIPT_BARRIER_ID } from './constants'\nimport type { AnyRouter } from '../router'\n\nexport function transformReadableStreamWithRouter(\n router: AnyRouter,\n routerStream: ReadableStream,\n) {\n return transformStreamWithRouter(router, routerStream)\n}\n\nexport function transformPipeableStreamWithRouter(\n router: AnyRouter,\n routerStream: Readable,\n) {\n return Readable.fromWeb(\n transformStreamWithRouter(router, Readable.toWeb(routerStream)),\n )\n}\n\n// Use string constants for simple indexOf matching\nconst BODY_END_TAG = '</body>'\nconst HTML_END_TAG = '</html>'\n\n// Minimum length of a valid closing tag: </a> = 4 characters\nconst MIN_CLOSING_TAG_LENGTH = 4\n\n// Default timeout values (in milliseconds)\nconst DEFAULT_SERIALIZATION_TIMEOUT_MS = 60000\nconst DEFAULT_LIFETIME_TIMEOUT_MS = 60000\n\n// Module-level encoder (stateless, safe to reuse)\nconst textEncoder = new TextEncoder()\n\n/**\n * Finds the position just after the last valid HTML closing tag in the string.\n *\n * Valid closing tags match the pattern: </[a-zA-Z][\\w:.-]*>\n * Examples: </div>, </my-component>, </slot:name.nested>\n *\n * @returns Position after the last closing tag, or -1 if none found\n */\nfunction findLastClosingTagEnd(str: string): number {\n const len = str.length\n if (len < MIN_CLOSING_TAG_LENGTH) return -1\n\n let i = len - 1\n\n while (i >= MIN_CLOSING_TAG_LENGTH - 1) {\n // Look for > (charCode 62)\n if (str.charCodeAt(i) === 62) {\n // Look backwards for valid tag name characters\n let j = i - 1\n\n // Skip through valid tag name characters\n while (j >= 1) {\n const code = str.charCodeAt(j)\n // Check if it's a valid tag name char: [a-zA-Z0-9_:.-]\n if (\n (code >= 97 && code <= 122) || // a-z\n (code >= 65 && code <= 90) || // A-Z\n (code >= 48 && code <= 57) || // 0-9\n code === 95 || // _\n code === 58 || // :\n code === 46 || // .\n code === 45 // -\n ) {\n j--\n } else {\n break\n }\n }\n\n // Check if the first char after </ is a valid start char (letter only)\n const tagNameStart = j + 1\n if (tagNameStart < i) {\n const startCode = str.charCodeAt(tagNameStart)\n // Tag name must start with a letter (a-z or A-Z)\n if (\n (startCode >= 97 && startCode <= 122) ||\n (startCode >= 65 && startCode <= 90)\n ) {\n // Check for </ (charCodes: < = 60, / = 47)\n if (\n j >= 1 &&\n str.charCodeAt(j) === 47 &&\n str.charCodeAt(j - 1) === 60\n ) {\n return i + 1 // Return position after the closing >\n }\n }\n }\n }\n i--\n }\n return -1\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n opts?: {\n /** Timeout for serialization to complete after app render finishes (default: 60000ms) */\n timeoutMs?: number\n /** Maximum lifetime of the stream transform (default: 60000ms). Safety net for cleanup. */\n lifetimeMs?: number\n },\n) {\n let stopListeningToInjectedHtml: (() => void) | undefined\n let stopListeningToSerializationFinished: (() => void) | undefined\n let serializationTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n let cleanedUp = false\n\n let controller: ReadableStreamDefaultController<any>\n let isStreamClosed = false\n\n // Check upfront if serialization already finished synchronously\n // This is the fast path for routes with no deferred data\n const serializationAlreadyFinished =\n router.serverSsr?.isSerializationFinished() ?? false\n\n /**\n * Cleanup function with guards against multiple calls.\n * Unsubscribes listeners, clears timeouts, frees buffers, and cleans up router SSR state.\n */\n function cleanup() {\n // Guard against multiple cleanup calls - set flag first to prevent re-entry\n if (cleanedUp) return\n cleanedUp = true\n\n // Unsubscribe listeners first (wrap in try-catch for safety)\n try {\n stopListeningToInjectedHtml?.()\n stopListeningToSerializationFinished?.()\n } catch (e) {\n // Ignore errors during unsubscription\n }\n stopListeningToInjectedHtml = undefined\n stopListeningToSerializationFinished = undefined\n\n // Clear all timeouts\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n if (lifetimeTimeoutHandle !== undefined) {\n clearTimeout(lifetimeTimeoutHandle)\n lifetimeTimeoutHandle = undefined\n }\n\n // Clear buffers to free memory\n pendingRouterHtmlParts = []\n leftover = ''\n pendingClosingTags = ''\n\n // Clean up router SSR state (has its own guard)\n router.serverSsr?.cleanup()\n }\n\n const textDecoder = new TextDecoder()\n\n function safeEnqueue(chunk: string | Uint8Array) {\n if (isStreamClosed) return\n if (typeof chunk === 'string') {\n controller.enqueue(textEncoder.encode(chunk))\n } else {\n controller.enqueue(chunk)\n }\n }\n\n function safeClose() {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller.close()\n } catch {\n // Stream may already be errored or closed by consumer - safe to ignore\n }\n }\n\n function safeError(error: unknown) {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller.error(error)\n } catch {\n // Stream may already be errored or closed by consumer - safe to ignore\n }\n }\n\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n cancel() {\n isStreamClosed = true\n cleanup()\n },\n })\n\n let isAppRendering = true\n let streamBarrierLifted = false\n let leftover = ''\n let pendingClosingTags = ''\n let serializationFinished = serializationAlreadyFinished\n\n let pendingRouterHtmlParts: Array<string> = []\n\n // Take any HTML that was buffered before we started listening\n const bufferedHtml = router.serverSsr?.takeBufferedHtml()\n if (bufferedHtml) {\n pendingRouterHtmlParts.push(bufferedHtml)\n }\n\n function flushPendingRouterHtml() {\n if (pendingRouterHtmlParts.length > 0) {\n safeEnqueue(pendingRouterHtmlParts.join(''))\n pendingRouterHtmlParts = []\n }\n }\n\n /**\n * Attempts to finish the stream if all conditions are met.\n */\n function tryFinish() {\n // Can only finish when app is done rendering and serialization is complete\n if (isAppRendering || !serializationFinished) return\n if (cleanedUp || isStreamClosed) return\n\n // Clear serialization timeout since we're finishing\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n\n // Flush any remaining bytes in the TextDecoder\n const decoderRemainder = textDecoder.decode()\n\n if (leftover) safeEnqueue(leftover)\n if (decoderRemainder) safeEnqueue(decoderRemainder)\n flushPendingRouterHtml()\n if (pendingClosingTags) safeEnqueue(pendingClosingTags)\n\n safeClose()\n cleanup()\n }\n\n // Set up lifetime timeout as a safety net\n // This ensures cleanup happens even if the stream is never consumed or gets stuck\n const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS\n lifetimeTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.warn(\n `SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`,\n )\n safeError(new Error('Stream lifetime exceeded'))\n cleanup()\n }\n }, lifetimeMs)\n\n // Only set up listeners if serialization hasn't already finished\n // This avoids unnecessary subscriptions for the common case of no deferred data\n if (!serializationAlreadyFinished) {\n // Listen for injected HTML (for deferred data that resolves later)\n stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', () => {\n if (cleanedUp || isStreamClosed) return\n\n // Retrieve buffered HTML\n const html = router.serverSsr?.takeBufferedHtml()\n if (!html) return\n\n if (isAppRendering || leftover) {\n // Buffer when app is still rendering OR when there's leftover content\n // that hasn't been flushed yet. This prevents race conditions where\n // injected HTML appears before buffered app content\n pendingRouterHtmlParts.push(html)\n } else {\n // App done rendering and no leftover - safe to write directly for better streaming\n safeEnqueue(html)\n }\n })\n\n // Listen for serialization finished\n stopListeningToSerializationFinished = router.subscribe(\n 'onSerializationFinished',\n () => {\n serializationFinished = true\n tryFinish()\n },\n )\n }\n\n // Transform the appStream\n ;(async () => {\n const reader = appStream.getReader()\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n // Don't process if already cleaned up\n if (cleanedUp || isStreamClosed) return\n\n const text =\n value instanceof Uint8Array\n ? textDecoder.decode(value, { stream: true })\n : String(value)\n const chunkString = leftover + text\n\n // Check for stream barrier (script placeholder) - use indexOf for efficiency\n if (!streamBarrierLifted) {\n if (chunkString.includes(TSR_SCRIPT_BARRIER_ID)) {\n streamBarrierLifted = true\n router.serverSsr?.liftScriptBarrier()\n }\n }\n\n // Check for body/html end tags\n const bodyEndIndex = chunkString.indexOf(BODY_END_TAG)\n const htmlEndIndex = chunkString.indexOf(HTML_END_TAG)\n\n // If we have both </body> and </html> in proper order,\n // insert router HTML before </body> and hold the closing tags\n if (\n bodyEndIndex !== -1 &&\n htmlEndIndex !== -1 &&\n bodyEndIndex < htmlEndIndex\n ) {\n pendingClosingTags = chunkString.slice(bodyEndIndex)\n\n safeEnqueue(chunkString.slice(0, bodyEndIndex))\n flushPendingRouterHtml()\n\n leftover = ''\n continue\n }\n\n // Handling partial closing tags split across chunks:\n //\n // Since `chunkString = leftover + text`, any incomplete tag fragment from the\n // previous chunk is prepended to the current chunk, allowing split tags like\n // \"</di\" + \"v>\" to be re-detected as a complete \"</div>\" in the combined string.\n //\n // - If a closing tag IS found (lastClosingTagEnd > 0): We enqueue content up to\n // the end of that tag, flush router HTML, and store the remainder in `leftover`.\n // This remainder may contain a partial tag (e.g., \"</sp\") which will be\n // prepended to the next chunk for re-detection.\n //\n // - If NO closing tag is found: The entire chunk is buffered in `leftover` and\n // will be prepended to the next chunk. This ensures partial tags are never\n // lost and will be detected once the rest of the tag arrives.\n //\n // This approach guarantees correct injection points even when closing tags span\n // chunk boundaries.\n const lastClosingTagEnd = findLastClosingTagEnd(chunkString)\n\n if (lastClosingTagEnd > 0) {\n // Found a closing tag - insert router HTML after it\n safeEnqueue(chunkString.slice(0, lastClosingTagEnd))\n flushPendingRouterHtml()\n\n leftover = chunkString.slice(lastClosingTagEnd)\n } else {\n // No closing tag found - buffer the entire chunk\n leftover = chunkString\n // Any pending router HTML will be inserted when we find a valid position\n }\n }\n\n // Stream ended\n if (cleanedUp || isStreamClosed) return\n\n // Mark the app as done rendering\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n\n // Try to finish if serialization is already done\n if (serializationFinished) {\n tryFinish()\n } else {\n // Set a timeout for serialization to complete\n const timeoutMs = opts?.timeoutMs ?? DEFAULT_SERIALIZATION_TIMEOUT_MS\n serializationTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.error('Serialization timeout after app render finished')\n safeError(\n new Error('Serialization timeout after app render finished'),\n )\n cleanup()\n }\n }, timeoutMs)\n }\n } catch (error) {\n if (cleanedUp) return\n console.error('Error reading appStream:', error)\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n safeError(error)\n cleanup()\n } finally {\n reader.releaseLock()\n }\n })().catch((error) => {\n // Handle any errors that occur outside the try block (e.g., getReader() failure)\n if (cleanedUp) return\n console.error('Error in stream transform:', error)\n safeError(error)\n cleanup()\n })\n\n return stream\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"transformStreamWithRouter.js","sources":["../../../src/ssr/transformStreamWithRouter.ts"],"sourcesContent":["import { ReadableStream } from 'node:stream/web'\nimport { Readable } from 'node:stream'\nimport { TSR_SCRIPT_BARRIER_ID } from './constants'\nimport type { AnyRouter } from '../router'\n\nexport function transformReadableStreamWithRouter(\n router: AnyRouter,\n routerStream: ReadableStream,\n) {\n return transformStreamWithRouter(router, routerStream)\n}\n\nexport function transformPipeableStreamWithRouter(\n router: AnyRouter,\n routerStream: Readable,\n) {\n return Readable.fromWeb(\n transformStreamWithRouter(router, Readable.toWeb(routerStream)),\n )\n}\n\n// Use string constants for simple indexOf matching\nconst BODY_END_TAG = '</body>'\nconst HTML_END_TAG = '</html>'\n\n// Minimum length of a valid closing tag: </a> = 4 characters\nconst MIN_CLOSING_TAG_LENGTH = 4\n\n// Default timeout values (in milliseconds)\nconst DEFAULT_SERIALIZATION_TIMEOUT_MS = 60000\nconst DEFAULT_LIFETIME_TIMEOUT_MS = 60000\n\n// Module-level encoder (stateless, safe to reuse)\nconst textEncoder = new TextEncoder()\n\n/**\n * Finds the position just after the last valid HTML closing tag in the string.\n *\n * Valid closing tags match the pattern: </[a-zA-Z][\\w:.-]*>\n * Examples: </div>, </my-component>, </slot:name.nested>\n *\n * @returns Position after the last closing tag, or -1 if none found\n */\nfunction findLastClosingTagEnd(str: string): number {\n const len = str.length\n if (len < MIN_CLOSING_TAG_LENGTH) return -1\n\n let i = len - 1\n\n while (i >= MIN_CLOSING_TAG_LENGTH - 1) {\n // Look for > (charCode 62)\n if (str.charCodeAt(i) === 62) {\n // Look backwards for valid tag name characters\n let j = i - 1\n\n // Skip through valid tag name characters\n while (j >= 1) {\n const code = str.charCodeAt(j)\n // Check if it's a valid tag name char: [a-zA-Z0-9_:.-]\n if (\n (code >= 97 && code <= 122) || // a-z\n (code >= 65 && code <= 90) || // A-Z\n (code >= 48 && code <= 57) || // 0-9\n code === 95 || // _\n code === 58 || // :\n code === 46 || // .\n code === 45 // -\n ) {\n j--\n } else {\n break\n }\n }\n\n // Check if the first char after </ is a valid start char (letter only)\n const tagNameStart = j + 1\n if (tagNameStart < i) {\n const startCode = str.charCodeAt(tagNameStart)\n // Tag name must start with a letter (a-z or A-Z)\n if (\n (startCode >= 97 && startCode <= 122) ||\n (startCode >= 65 && startCode <= 90)\n ) {\n // Check for </ (charCodes: < = 60, / = 47)\n if (\n j >= 1 &&\n str.charCodeAt(j) === 47 &&\n str.charCodeAt(j - 1) === 60\n ) {\n return i + 1 // Return position after the closing >\n }\n }\n }\n }\n i--\n }\n return -1\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n opts?: {\n /** Timeout for serialization to complete after app render finishes (default: 60000ms) */\n timeoutMs?: number\n /** Maximum lifetime of the stream transform (default: 60000ms). Safety net for cleanup. */\n lifetimeMs?: number\n },\n) {\n // Check upfront if serialization already finished synchronously\n // This is the fast path for routes with no deferred data\n const serializationAlreadyFinished =\n router.serverSsr?.isSerializationFinished() ?? false\n\n // Take any HTML that was buffered before we started listening\n const initialBufferedHtml = router.serverSsr?.takeBufferedHtml()\n\n // True passthrough: if serialization already finished and nothing buffered,\n // we can avoid any decoding/scanning while still honoring cleanup + setRenderFinished.\n if (serializationAlreadyFinished && !initialBufferedHtml) {\n let cleanedUp = false\n let controller: ReadableStreamDefaultController<Uint8Array> | undefined\n let isStreamClosed = false\n let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n\n const cleanup = () => {\n if (cleanedUp) return\n cleanedUp = true\n\n if (lifetimeTimeoutHandle !== undefined) {\n clearTimeout(lifetimeTimeoutHandle)\n lifetimeTimeoutHandle = undefined\n }\n\n router.serverSsr?.cleanup()\n }\n\n const safeClose = () => {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller?.close()\n } catch {\n // ignore\n }\n }\n\n const safeError = (error: unknown) => {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller?.error(error)\n } catch {\n // ignore\n }\n }\n\n const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS\n lifetimeTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.warn(\n `SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`,\n )\n safeError(new Error('Stream lifetime exceeded'))\n cleanup()\n }\n }, lifetimeMs)\n\n const stream = new ReadableStream<Uint8Array>({\n start(c) {\n controller = c\n },\n cancel() {\n isStreamClosed = true\n cleanup()\n },\n })\n\n ;(async () => {\n const reader = appStream.getReader()\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n if (cleanedUp || isStreamClosed) return\n controller?.enqueue(value as unknown as Uint8Array)\n }\n\n if (cleanedUp || isStreamClosed) return\n\n router.serverSsr?.setRenderFinished()\n safeClose()\n cleanup()\n } catch (error) {\n if (cleanedUp) return\n console.error('Error reading appStream:', error)\n router.serverSsr?.setRenderFinished()\n safeError(error)\n cleanup()\n } finally {\n reader.releaseLock()\n }\n })().catch((error) => {\n if (cleanedUp) return\n console.error('Error in stream transform:', error)\n safeError(error)\n cleanup()\n })\n\n return stream\n }\n\n let stopListeningToInjectedHtml: (() => void) | undefined\n let stopListeningToSerializationFinished: (() => void) | undefined\n let serializationTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n let cleanedUp = false\n\n let controller: ReadableStreamDefaultController<any>\n let isStreamClosed = false\n\n const textDecoder = new TextDecoder()\n\n // concat'd router HTML; avoids array joins on each flush\n let pendingRouterHtml = initialBufferedHtml ?? ''\n\n // between-chunk text buffer; keep bounded to avoid unbounded memory\n let leftover = ''\n\n // captured closing tags from </body> onward\n let pendingClosingTags = ''\n\n // conservative cap: enough to hold any partial closing tag + a bit\n const MAX_LEFTOVER_CHARS = 2048\n\n let isAppRendering = true\n let streamBarrierLifted = false\n let serializationFinished = serializationAlreadyFinished\n\n function safeEnqueue(chunk: string | Uint8Array) {\n if (isStreamClosed) return\n if (typeof chunk === 'string') {\n controller.enqueue(textEncoder.encode(chunk))\n } else {\n controller.enqueue(chunk)\n }\n }\n\n function safeClose() {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller.close()\n } catch {\n // ignore\n }\n }\n\n function safeError(error: unknown) {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller.error(error)\n } catch {\n // ignore\n }\n }\n\n /**\n * Cleanup with guards; must be idempotent.\n */\n function cleanup() {\n if (cleanedUp) return\n cleanedUp = true\n\n try {\n stopListeningToInjectedHtml?.()\n stopListeningToSerializationFinished?.()\n } catch {\n // ignore\n }\n stopListeningToInjectedHtml = undefined\n stopListeningToSerializationFinished = undefined\n\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n if (lifetimeTimeoutHandle !== undefined) {\n clearTimeout(lifetimeTimeoutHandle)\n lifetimeTimeoutHandle = undefined\n }\n\n pendingRouterHtml = ''\n leftover = ''\n pendingClosingTags = ''\n\n router.serverSsr?.cleanup()\n }\n\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n cancel() {\n isStreamClosed = true\n cleanup()\n },\n })\n\n function flushPendingRouterHtml() {\n if (!pendingRouterHtml) return\n safeEnqueue(pendingRouterHtml)\n pendingRouterHtml = ''\n }\n\n function appendRouterHtml(html: string) {\n if (!html) return\n pendingRouterHtml += html\n }\n\n /**\n * Finish only when app done and serialization complete.\n */\n function tryFinish() {\n if (isAppRendering || !serializationFinished) return\n if (cleanedUp || isStreamClosed) return\n\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n\n // Flush any remaining bytes in the TextDecoder\n const decoderRemainder = textDecoder.decode()\n\n if (leftover) safeEnqueue(leftover)\n if (decoderRemainder) safeEnqueue(decoderRemainder)\n flushPendingRouterHtml()\n if (pendingClosingTags) safeEnqueue(pendingClosingTags)\n\n safeClose()\n cleanup()\n }\n\n // Safety net: cleanup even if consumer never reads\n const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS\n lifetimeTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.warn(\n `SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`,\n )\n safeError(new Error('Stream lifetime exceeded'))\n cleanup()\n }\n }, lifetimeMs)\n\n if (!serializationAlreadyFinished) {\n stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', () => {\n if (cleanedUp || isStreamClosed) return\n const html = router.serverSsr?.takeBufferedHtml()\n if (!html) return\n\n // If we've already captured </body> (pendingClosingTags), we must keep appending\n // so injection stays before the stored closing tags.\n if (isAppRendering || leftover || pendingClosingTags) {\n appendRouterHtml(html)\n } else {\n safeEnqueue(html)\n }\n })\n\n stopListeningToSerializationFinished = router.subscribe(\n 'onSerializationFinished',\n () => {\n serializationFinished = true\n tryFinish()\n },\n )\n }\n\n // Transform the appStream\n ;(async () => {\n const reader = appStream.getReader()\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n if (cleanedUp || isStreamClosed) return\n\n const text =\n value instanceof Uint8Array\n ? textDecoder.decode(value, { stream: true })\n : String(value)\n\n // Fast path: most chunks have no pending left-over.\n const chunkString = leftover ? leftover + text : text\n\n if (!streamBarrierLifted) {\n if (chunkString.includes(TSR_SCRIPT_BARRIER_ID)) {\n streamBarrierLifted = true\n router.serverSsr?.liftScriptBarrier()\n }\n }\n\n // If we already saw </body>, everything else is part of tail; buffer it.\n if (pendingClosingTags) {\n pendingClosingTags += chunkString\n leftover = ''\n continue\n }\n\n const bodyEndIndex = chunkString.indexOf(BODY_END_TAG)\n const htmlEndIndex = chunkString.indexOf(HTML_END_TAG)\n\n if (\n bodyEndIndex !== -1 &&\n htmlEndIndex !== -1 &&\n bodyEndIndex < htmlEndIndex\n ) {\n pendingClosingTags = chunkString.slice(bodyEndIndex)\n safeEnqueue(chunkString.slice(0, bodyEndIndex))\n flushPendingRouterHtml()\n leftover = ''\n continue\n }\n\n const lastClosingTagEnd = findLastClosingTagEnd(chunkString)\n\n if (lastClosingTagEnd > 0) {\n safeEnqueue(chunkString.slice(0, lastClosingTagEnd))\n flushPendingRouterHtml()\n\n leftover = chunkString.slice(lastClosingTagEnd)\n if (leftover.length > MAX_LEFTOVER_CHARS) {\n // Ensure bounded memory even if a consumer streams long text sequences\n // without any closing tags. This may reduce injection granularity but is correct.\n safeEnqueue(leftover.slice(0, leftover.length - MAX_LEFTOVER_CHARS))\n leftover = leftover.slice(-MAX_LEFTOVER_CHARS)\n }\n } else {\n // No closing tag found; keep small tail to handle split closing tags,\n // but stream older bytes to prevent unbounded buffering.\n const combined = chunkString\n if (combined.length > MAX_LEFTOVER_CHARS) {\n const flushUpto = combined.length - MAX_LEFTOVER_CHARS\n safeEnqueue(combined.slice(0, flushUpto))\n leftover = combined.slice(flushUpto)\n } else {\n leftover = combined\n }\n }\n }\n\n if (cleanedUp || isStreamClosed) return\n\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n\n if (serializationFinished) {\n tryFinish()\n } else {\n const timeoutMs = opts?.timeoutMs ?? DEFAULT_SERIALIZATION_TIMEOUT_MS\n serializationTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.error('Serialization timeout after app render finished')\n safeError(\n new Error('Serialization timeout after app render finished'),\n )\n cleanup()\n }\n }, timeoutMs)\n }\n } catch (error) {\n if (cleanedUp) return\n console.error('Error reading appStream:', error)\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n safeError(error)\n cleanup()\n } finally {\n reader.releaseLock()\n }\n })().catch((error) => {\n if (cleanedUp) return\n console.error('Error in stream transform:', error)\n safeError(error)\n cleanup()\n })\n\n return stream\n}\n"],"names":["cleanedUp","controller","isStreamClosed","lifetimeTimeoutHandle","cleanup","safeClose","safeError","lifetimeMs","stream"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function batch<T>(fn: () => T): T;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { batch as batch$1 } from "@tanstack/store";
|
|
2
|
+
import { isServer } from "@tanstack/router-core/isServer";
|
|
3
|
+
function batch(fn) {
|
|
4
|
+
if (isServer) {
|
|
5
|
+
return fn();
|
|
6
|
+
}
|
|
7
|
+
let result;
|
|
8
|
+
batch$1(() => {
|
|
9
|
+
result = fn();
|
|
10
|
+
});
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
batch
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=batch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.js","sources":["../../../src/utils/batch.ts"],"sourcesContent":["import { batch as storeBatch } from '@tanstack/store'\n\nimport { isServer } from '@tanstack/router-core/isServer'\n\n// `@tanstack/store`'s `batch` is for reactive notification batching.\n// On the server we don't subscribe/render reactively, so a lightweight\n// implementation that just executes is enough.\nexport function batch<T>(fn: () => T): T {\n if (isServer) {\n return fn()\n }\n\n let result!: T\n storeBatch(() => {\n result = fn()\n })\n return result\n}\n"],"names":["storeBatch"],"mappings":";;AAOO,SAAS,MAAS,IAAgB;AACvC,MAAI,UAAU;AACZ,WAAO,GAAA;AAAA,EACT;AAEA,MAAI;AACJA,UAAW,MAAM;AACf,aAAS,GAAA;AAAA,EACX,CAAC;AACD,SAAO;AACT;"}
|
package/dist/esm/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isServer } from "@tanstack/router-core/isServer";
|
|
1
2
|
function last(arr) {
|
|
2
3
|
return arr[arr.length - 1];
|
|
3
4
|
}
|
|
@@ -13,6 +14,9 @@ function functionalUpdate(updater, previous) {
|
|
|
13
14
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
14
15
|
const isEnumerable = Object.prototype.propertyIsEnumerable;
|
|
15
16
|
function replaceEqualDeep(prev, _next, _depth = 0) {
|
|
17
|
+
if (isServer) {
|
|
18
|
+
return _next;
|
|
19
|
+
}
|
|
16
20
|
if (prev === _next) {
|
|
17
21
|
return prev;
|
|
18
22
|
}
|
package/dist/esm/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import type { RouteIds } from './routeInfo'\nimport type { AnyRouter } from './router'\n\nexport type Awaitable<T> = T | Promise<T>\nexport type NoInfer<T> = [T][T extends any ? 0 : never]\nexport type IsAny<TValue, TYesResult, TNoResult = TValue> = 1 extends 0 & TValue\n ? TYesResult\n : TNoResult\n\nexport type PickAsRequired<TValue, TKey extends keyof TValue> = Omit<\n TValue,\n TKey\n> &\n Required<Pick<TValue, TKey>>\n\nexport type PickRequired<T> = {\n [K in keyof T as undefined extends T[K] ? never : K]: T[K]\n}\n\nexport type PickOptional<T> = {\n [K in keyof T as undefined extends T[K] ? K : never]: T[K]\n}\n\n// from https://stackoverflow.com/a/76458160\nexport type WithoutEmpty<T> = T extends any ? ({} extends T ? never : T) : never\n\nexport type Expand<T> = T extends object\n ? T extends infer O\n ? O extends Function\n ? O\n : { [K in keyof O]: O[K] }\n : never\n : T\n\nexport type DeepPartial<T> = T extends object\n ? {\n [P in keyof T]?: DeepPartial<T[P]>\n }\n : T\n\nexport type MakeDifferenceOptional<TLeft, TRight> = keyof TLeft &\n keyof TRight extends never\n ? TRight\n : Omit<TRight, keyof TLeft & keyof TRight> & {\n [K in keyof TLeft & keyof TRight]?: TRight[K]\n }\n\n// from https://stackoverflow.com/a/53955431\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type IsUnion<T, U extends T = T> = (\n T extends any ? (U extends T ? false : true) : never\n) extends false\n ? false\n : true\n\nexport type IsNonEmptyObject<T> = T extends object\n ? keyof T extends never\n ? false\n : true\n : false\n\nexport type Assign<TLeft, TRight> = TLeft extends any\n ? TRight extends any\n ? IsNonEmptyObject<TLeft> extends false\n ? TRight\n : IsNonEmptyObject<TRight> extends false\n ? TLeft\n : keyof TLeft & keyof TRight extends never\n ? TLeft & TRight\n : Omit<TLeft, keyof TRight> & TRight\n : never\n : never\n\nexport type IntersectAssign<TLeft, TRight> = TLeft extends any\n ? TRight extends any\n ? IsNonEmptyObject<TLeft> extends false\n ? TRight\n : IsNonEmptyObject<TRight> extends false\n ? TLeft\n : TRight & TLeft\n : never\n : never\n\nexport type Timeout = ReturnType<typeof setTimeout>\n\nexport type Updater<TPrevious, TResult = TPrevious> =\n | TResult\n | ((prev?: TPrevious) => TResult)\n\nexport type NonNullableUpdater<TPrevious, TResult = TPrevious> =\n | TResult\n | ((prev: TPrevious) => TResult)\n\nexport type ExtractObjects<TUnion> = TUnion extends MergeAllPrimitive\n ? never\n : TUnion\n\nexport type PartialMergeAllObject<TUnion> =\n ExtractObjects<TUnion> extends infer TObj\n ? [TObj] extends [never]\n ? never\n : {\n [TKey in TObj extends any ? keyof TObj : never]?: TObj extends any\n ? TKey extends keyof TObj\n ? TObj[TKey]\n : never\n : never\n }\n : never\n\nexport type MergeAllPrimitive =\n | ReadonlyArray<any>\n | number\n | string\n | bigint\n | boolean\n | symbol\n | undefined\n | null\n\nexport type ExtractPrimitives<TUnion> = TUnion extends MergeAllPrimitive\n ? TUnion\n : TUnion extends object\n ? never\n : TUnion\n\nexport type PartialMergeAll<TUnion> =\n | ExtractPrimitives<TUnion>\n | PartialMergeAllObject<TUnion>\n\nexport type Constrain<T, TConstraint, TDefault = TConstraint> =\n | (T extends TConstraint ? T : never)\n | TDefault\n\nexport type ConstrainLiteral<T, TConstraint, TDefault = TConstraint> =\n | (T & TConstraint)\n | TDefault\n\n/**\n * To be added to router types\n */\nexport type UnionToIntersection<T> = (\n T extends any ? (arg: T) => any : never\n) extends (arg: infer T) => any\n ? T\n : never\n\n/**\n * Merges everything in a union into one object.\n * This mapped type is homomorphic which means it preserves stuff! :)\n */\nexport type MergeAllObjects<\n TUnion,\n TIntersected = UnionToIntersection<ExtractObjects<TUnion>>,\n> = [keyof TIntersected] extends [never]\n ? never\n : {\n [TKey in keyof TIntersected]: TUnion extends any\n ? TUnion[TKey & keyof TUnion]\n : never\n }\n\nexport type MergeAll<TUnion> =\n | MergeAllObjects<TUnion>\n | ExtractPrimitives<TUnion>\n\nexport type ValidateJSON<T> = ((...args: Array<any>) => any) extends T\n ? unknown extends T\n ? never\n : 'Function is not serializable'\n : { [K in keyof T]: ValidateJSON<T[K]> }\n\nexport type LooseReturnType<T> = T extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn\n : never\n\nexport type LooseAsyncReturnType<T> = T extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn extends Promise<infer TReturn>\n ? TReturn\n : TReturn\n : never\n\n/**\n * Return the last element of an array.\n * Intended for non-empty arrays used within router internals.\n */\nexport function last<T>(arr: ReadonlyArray<T>) {\n return arr[arr.length - 1]\n}\n\nfunction isFunction(d: any): d is Function {\n return typeof d === 'function'\n}\n\n/**\n * Apply a value-or-updater to a previous value.\n * Accepts either a literal value or a function of the previous value.\n */\nexport function functionalUpdate<TPrevious, TResult = TPrevious>(\n updater: Updater<TPrevious, TResult> | NonNullableUpdater<TPrevious, TResult>,\n previous: TPrevious,\n): TResult {\n if (isFunction(updater)) {\n return updater(previous)\n }\n\n return updater\n}\n\nconst hasOwn = Object.prototype.hasOwnProperty\nconst isEnumerable = Object.prototype.propertyIsEnumerable\n\n/**\n * This function returns `prev` if `_next` is deeply equal.\n * If not, it will replace any deeply equal children of `b` with those of `a`.\n * This can be used for structural sharing between immutable JSON values for example.\n * Do not use this with signals\n */\nexport function replaceEqualDeep<T>(prev: any, _next: T, _depth = 0): T {\n if (prev === _next) {\n return prev\n }\n\n if (_depth > 500) return _next\n\n const next = _next as any\n\n const array = isPlainArray(prev) && isPlainArray(next)\n\n if (!array && !(isPlainObject(prev) && isPlainObject(next))) return next\n\n const prevItems = array ? prev : getEnumerableOwnKeys(prev)\n if (!prevItems) return next\n const nextItems = array ? next : getEnumerableOwnKeys(next)\n if (!nextItems) return next\n const prevSize = prevItems.length\n const nextSize = nextItems.length\n const copy: any = array ? new Array(nextSize) : {}\n\n let equalItems = 0\n\n for (let i = 0; i < nextSize; i++) {\n const key = array ? i : (nextItems[i] as any)\n const p = prev[key]\n const n = next[key]\n\n if (p === n) {\n copy[key] = p\n if (array ? i < prevSize : hasOwn.call(prev, key)) equalItems++\n continue\n }\n\n if (\n p === null ||\n n === null ||\n typeof p !== 'object' ||\n typeof n !== 'object'\n ) {\n copy[key] = n\n continue\n }\n\n const v = replaceEqualDeep(p, n, _depth + 1)\n copy[key] = v\n if (v === p) equalItems++\n }\n\n return prevSize === nextSize && equalItems === prevSize ? prev : copy\n}\n\n/**\n * Equivalent to `Reflect.ownKeys`, but ensures that objects are \"clone-friendly\":\n * will return false if object has any non-enumerable properties.\n *\n * Optimized for the common case where objects have no symbol properties.\n */\nfunction getEnumerableOwnKeys(o: object) {\n const names = Object.getOwnPropertyNames(o)\n\n // Fast path: check all string property names are enumerable\n for (const name of names) {\n if (!isEnumerable.call(o, name)) return false\n }\n\n // Only check symbols if the object has any (most plain objects don't)\n const symbols = Object.getOwnPropertySymbols(o)\n\n // Fast path: no symbols, return names directly (avoids array allocation/concat)\n if (symbols.length === 0) return names\n\n // Slow path: has symbols, need to check and merge\n const keys: Array<string | symbol> = names\n for (const symbol of symbols) {\n if (!isEnumerable.call(o, symbol)) return false\n keys.push(symbol)\n }\n return keys\n}\n\n// Copied from: https://github.com/jonschlinkert/is-plain-object\nexport function isPlainObject(o: any) {\n if (!hasObjectPrototype(o)) {\n return false\n }\n\n // If has modified constructor\n const ctor = o.constructor\n if (typeof ctor === 'undefined') {\n return true\n }\n\n // If has modified prototype\n const prot = ctor.prototype\n if (!hasObjectPrototype(prot)) {\n return false\n }\n\n // If constructor does not have an Object-specific method\n if (!prot.hasOwnProperty('isPrototypeOf')) {\n return false\n }\n\n // Most likely a plain Object\n return true\n}\n\nfunction hasObjectPrototype(o: any) {\n return Object.prototype.toString.call(o) === '[object Object]'\n}\n\n/**\n * Check if a value is a \"plain\" array (no extra enumerable keys).\n */\nexport function isPlainArray(value: unknown): value is Array<unknown> {\n return Array.isArray(value) && value.length === Object.keys(value).length\n}\n\n/**\n * Perform a deep equality check with options for partial comparison and\n * ignoring `undefined` values. Optimized for router state comparisons.\n */\nexport function deepEqual(\n a: any,\n b: any,\n opts?: { partial?: boolean; ignoreUndefined?: boolean },\n): boolean {\n if (a === b) {\n return true\n }\n\n if (typeof a !== typeof b) {\n return false\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let i = 0, l = a.length; i < l; i++) {\n if (!deepEqual(a[i], b[i], opts)) return false\n }\n return true\n }\n\n if (isPlainObject(a) && isPlainObject(b)) {\n const ignoreUndefined = opts?.ignoreUndefined ?? true\n\n if (opts?.partial) {\n for (const k in b) {\n if (!ignoreUndefined || b[k] !== undefined) {\n if (!deepEqual(a[k], b[k], opts)) return false\n }\n }\n return true\n }\n\n let aCount = 0\n if (!ignoreUndefined) {\n aCount = Object.keys(a).length\n } else {\n for (const k in a) {\n if (a[k] !== undefined) aCount++\n }\n }\n\n let bCount = 0\n for (const k in b) {\n if (!ignoreUndefined || b[k] !== undefined) {\n bCount++\n if (bCount > aCount || !deepEqual(a[k], b[k], opts)) return false\n }\n }\n\n return aCount === bCount\n }\n\n return false\n}\n\nexport type StringLiteral<T> = T extends string\n ? string extends T\n ? string\n : T\n : never\n\nexport type ThrowOrOptional<T, TThrow extends boolean> = TThrow extends true\n ? T\n : T | undefined\n\nexport type StrictOrFrom<\n TRouter extends AnyRouter,\n TFrom,\n TStrict extends boolean = true,\n> = TStrict extends false\n ? {\n from?: never\n strict: TStrict\n }\n : {\n from: ConstrainLiteral<TFrom, RouteIds<TRouter['routeTree']>>\n strict?: TStrict\n }\n\nexport type ThrowConstraint<\n TStrict extends boolean,\n TThrow extends boolean,\n> = TStrict extends false ? (TThrow extends true ? never : TThrow) : TThrow\n\nexport type ControlledPromise<T> = Promise<T> & {\n resolve: (value: T) => void\n reject: (value: any) => void\n status: 'pending' | 'resolved' | 'rejected'\n value?: T\n}\n\n/**\n * Create a promise with exposed resolve/reject and status fields.\n * Useful for coordinating async router lifecycle operations.\n */\nexport function createControlledPromise<T>(onResolve?: (value: T) => void) {\n let resolveLoadPromise!: (value: T) => void\n let rejectLoadPromise!: (value: any) => void\n\n const controlledPromise = new Promise<T>((resolve, reject) => {\n resolveLoadPromise = resolve\n rejectLoadPromise = reject\n }) as ControlledPromise<T>\n\n controlledPromise.status = 'pending'\n\n controlledPromise.resolve = (value: T) => {\n controlledPromise.status = 'resolved'\n controlledPromise.value = value\n resolveLoadPromise(value)\n onResolve?.(value)\n }\n\n controlledPromise.reject = (e) => {\n controlledPromise.status = 'rejected'\n rejectLoadPromise(e)\n }\n\n return controlledPromise\n}\n\n/**\n * Heuristically detect dynamic import \"module not found\" errors\n * across major browsers for lazy route component handling.\n */\nexport function isModuleNotFoundError(error: any): boolean {\n // chrome: \"Failed to fetch dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split\"\n // firefox: \"error loading dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split\"\n // safari: \"Importing a module script failed.\"\n if (typeof error?.message !== 'string') return false\n return (\n error.message.startsWith('Failed to fetch dynamically imported module') ||\n error.message.startsWith('error loading dynamically imported module') ||\n error.message.startsWith('Importing a module script failed')\n )\n}\n\nexport function isPromise<T>(\n value: Promise<Awaited<T>> | T,\n): value is Promise<Awaited<T>> {\n return Boolean(\n value &&\n typeof value === 'object' &&\n typeof (value as Promise<T>).then === 'function',\n )\n}\n\nexport function findLast<T>(\n array: ReadonlyArray<T>,\n predicate: (item: T) => boolean,\n): T | undefined {\n for (let i = array.length - 1; i >= 0; i--) {\n const item = array[i]!\n if (predicate(item)) return item\n }\n return undefined\n}\n\n/**\n * Remove control characters that can cause open redirect vulnerabilities.\n * Characters like \\r (CR) and \\n (LF) can trick URL parsers into interpreting\n * paths like \"/\\r/evil.com\" as \"http://evil.com\".\n */\nfunction sanitizePathSegment(segment: string): string {\n // Remove ASCII control characters (0x00-0x1F) and DEL (0x7F)\n // These include CR (\\r = 0x0D), LF (\\n = 0x0A), and other potentially dangerous characters\n // eslint-disable-next-line no-control-regex\n return segment.replace(/[\\x00-\\x1f\\x7f]/g, '')\n}\n\nfunction decodeSegment(segment: string): string {\n let decoded: string\n try {\n decoded = decodeURI(segment)\n } catch {\n // if the decoding fails, try to decode the various parts leaving the malformed tags in place\n decoded = segment.replaceAll(/%[0-9A-F]{2}/gi, (match) => {\n try {\n return decodeURI(match)\n } catch {\n return match\n }\n })\n }\n return sanitizePathSegment(decoded)\n}\n\n/**\n * List of URL protocols that are safe for navigation.\n * Only these protocols are allowed in redirects and navigation.\n */\nexport const SAFE_URL_PROTOCOLS = ['http:', 'https:', 'mailto:', 'tel:']\n\n/**\n * Check if a URL string uses a protocol that is not in the safe list.\n * Returns true for dangerous protocols like javascript:, data:, vbscript:, etc.\n *\n * The URL constructor correctly normalizes:\n * - Mixed case (JavaScript: → javascript:)\n * - Whitespace/control characters (java\\nscript: → javascript:)\n * - Leading whitespace\n *\n * For relative URLs (no protocol), returns false (safe).\n *\n * @param url - The URL string to check\n * @returns true if the URL uses a dangerous (non-whitelisted) protocol\n */\nexport function isDangerousProtocol(url: string): boolean {\n if (!url) return false\n\n try {\n // Use the URL constructor - it correctly normalizes protocols\n // per WHATWG URL spec, handling all bypass attempts automatically\n const parsed = new URL(url)\n return !SAFE_URL_PROTOCOLS.includes(parsed.protocol)\n } catch {\n // URL constructor throws for relative URLs (no protocol)\n // These are safe - they can't execute scripts\n return false\n }\n}\n\n// This utility is based on https://github.com/zertosh/htmlescape\n// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE\nconst HTML_ESCAPE_LOOKUP: { [match: string]: string } = {\n '&': '\\\\u0026',\n '>': '\\\\u003e',\n '<': '\\\\u003c',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n}\n\nconst HTML_ESCAPE_REGEX = /[&><\\u2028\\u2029]/g\n\n/**\n * Escape HTML special characters in a string to prevent XSS attacks\n * when embedding strings in script tags during SSR.\n *\n * This is essential for preventing XSS vulnerabilities when user-controlled\n * content is embedded in inline scripts.\n */\nexport function escapeHtml(str: string): string {\n return str.replace(HTML_ESCAPE_REGEX, (match) => HTML_ESCAPE_LOOKUP[match]!)\n}\n\nexport function decodePath(path: string, decodeIgnore?: Array<string>): string {\n if (!path) return path\n const re = decodeIgnore\n ? new RegExp(`${decodeIgnore.join('|')}`, 'gi')\n : /%25|%5C/gi\n let cursor = 0\n let result = ''\n let match\n while (null !== (match = re.exec(path))) {\n result += decodeSegment(path.slice(cursor, match.index)) + match[0]\n cursor = re.lastIndex\n }\n result = result + decodeSegment(cursor ? path.slice(cursor) : path)\n\n // Prevent open redirect via protocol-relative URLs (e.g. \"//evil.com\")\n // After sanitizing control characters, paths like \"/\\r/evil.com\" become \"//evil.com\"\n // Collapse leading double slashes to a single slash\n if (result.startsWith('//')) {\n result = '/' + result.replace(/^\\/+/, '')\n }\n\n return result\n}\n\n/**\n * Encodes non-ASCII (unicode) characters in a path while preserving\n * already percent-encoded sequences. This is used to generate proper\n * href values without constructing URL objects.\n *\n * Unlike encodeURI, this won't double-encode percent-encoded sequences\n * like %2F or %25 because it only targets non-ASCII characters.\n */\nexport function encodeNonAscii(path: string): string {\n // eslint-disable-next-line no-control-regex\n if (!/[^\\u0000-\\u007F]/.test(path)) return path\n // eslint-disable-next-line no-control-regex\n return path.replace(/[^\\u0000-\\u007F]/gu, encodeURIComponent)\n}\n\n/**\n * Builds the dev-mode CSS styles URL for route-scoped CSS collection.\n * Used by HeadContent components in all framework implementations to construct\n * the URL for the `/@tanstack-start/styles.css` endpoint.\n *\n * @param basepath - The router's basepath (may or may not have leading slash)\n * @param routeIds - Array of matched route IDs to include in the CSS collection\n * @returns The full URL path for the dev styles CSS endpoint\n */\nexport function buildDevStylesUrl(\n basepath: string,\n routeIds: Array<string>,\n): string {\n // Trim all leading and trailing slashes from basepath\n const trimmedBasepath = basepath.replace(/^\\/+|\\/+$/g, '')\n // Build normalized basepath: empty string for root, or '/path' for non-root\n const normalizedBasepath = trimmedBasepath === '' ? '' : `/${trimmedBasepath}`\n return `${normalizedBasepath}/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds.join(','))}`\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport type { RouteIds } from './routeInfo'\nimport type { AnyRouter } from './router'\n\nexport type Awaitable<T> = T | Promise<T>\nexport type NoInfer<T> = [T][T extends any ? 0 : never]\nexport type IsAny<TValue, TYesResult, TNoResult = TValue> = 1 extends 0 & TValue\n ? TYesResult\n : TNoResult\n\nexport type PickAsRequired<TValue, TKey extends keyof TValue> = Omit<\n TValue,\n TKey\n> &\n Required<Pick<TValue, TKey>>\n\nexport type PickRequired<T> = {\n [K in keyof T as undefined extends T[K] ? never : K]: T[K]\n}\n\nexport type PickOptional<T> = {\n [K in keyof T as undefined extends T[K] ? K : never]: T[K]\n}\n\n// from https://stackoverflow.com/a/76458160\nexport type WithoutEmpty<T> = T extends any ? ({} extends T ? never : T) : never\n\nexport type Expand<T> = T extends object\n ? T extends infer O\n ? O extends Function\n ? O\n : { [K in keyof O]: O[K] }\n : never\n : T\n\nexport type DeepPartial<T> = T extends object\n ? {\n [P in keyof T]?: DeepPartial<T[P]>\n }\n : T\n\nexport type MakeDifferenceOptional<TLeft, TRight> = keyof TLeft &\n keyof TRight extends never\n ? TRight\n : Omit<TRight, keyof TLeft & keyof TRight> & {\n [K in keyof TLeft & keyof TRight]?: TRight[K]\n }\n\n// from https://stackoverflow.com/a/53955431\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type IsUnion<T, U extends T = T> = (\n T extends any ? (U extends T ? false : true) : never\n) extends false\n ? false\n : true\n\nexport type IsNonEmptyObject<T> = T extends object\n ? keyof T extends never\n ? false\n : true\n : false\n\nexport type Assign<TLeft, TRight> = TLeft extends any\n ? TRight extends any\n ? IsNonEmptyObject<TLeft> extends false\n ? TRight\n : IsNonEmptyObject<TRight> extends false\n ? TLeft\n : keyof TLeft & keyof TRight extends never\n ? TLeft & TRight\n : Omit<TLeft, keyof TRight> & TRight\n : never\n : never\n\nexport type IntersectAssign<TLeft, TRight> = TLeft extends any\n ? TRight extends any\n ? IsNonEmptyObject<TLeft> extends false\n ? TRight\n : IsNonEmptyObject<TRight> extends false\n ? TLeft\n : TRight & TLeft\n : never\n : never\n\nexport type Timeout = ReturnType<typeof setTimeout>\n\nexport type Updater<TPrevious, TResult = TPrevious> =\n | TResult\n | ((prev?: TPrevious) => TResult)\n\nexport type NonNullableUpdater<TPrevious, TResult = TPrevious> =\n | TResult\n | ((prev: TPrevious) => TResult)\n\nexport type ExtractObjects<TUnion> = TUnion extends MergeAllPrimitive\n ? never\n : TUnion\n\nexport type PartialMergeAllObject<TUnion> =\n ExtractObjects<TUnion> extends infer TObj\n ? [TObj] extends [never]\n ? never\n : {\n [TKey in TObj extends any ? keyof TObj : never]?: TObj extends any\n ? TKey extends keyof TObj\n ? TObj[TKey]\n : never\n : never\n }\n : never\n\nexport type MergeAllPrimitive =\n | ReadonlyArray<any>\n | number\n | string\n | bigint\n | boolean\n | symbol\n | undefined\n | null\n\nexport type ExtractPrimitives<TUnion> = TUnion extends MergeAllPrimitive\n ? TUnion\n : TUnion extends object\n ? never\n : TUnion\n\nexport type PartialMergeAll<TUnion> =\n | ExtractPrimitives<TUnion>\n | PartialMergeAllObject<TUnion>\n\nexport type Constrain<T, TConstraint, TDefault = TConstraint> =\n | (T extends TConstraint ? T : never)\n | TDefault\n\nexport type ConstrainLiteral<T, TConstraint, TDefault = TConstraint> =\n | (T & TConstraint)\n | TDefault\n\n/**\n * To be added to router types\n */\nexport type UnionToIntersection<T> = (\n T extends any ? (arg: T) => any : never\n) extends (arg: infer T) => any\n ? T\n : never\n\n/**\n * Merges everything in a union into one object.\n * This mapped type is homomorphic which means it preserves stuff! :)\n */\nexport type MergeAllObjects<\n TUnion,\n TIntersected = UnionToIntersection<ExtractObjects<TUnion>>,\n> = [keyof TIntersected] extends [never]\n ? never\n : {\n [TKey in keyof TIntersected]: TUnion extends any\n ? TUnion[TKey & keyof TUnion]\n : never\n }\n\nexport type MergeAll<TUnion> =\n | MergeAllObjects<TUnion>\n | ExtractPrimitives<TUnion>\n\nexport type ValidateJSON<T> = ((...args: Array<any>) => any) extends T\n ? unknown extends T\n ? never\n : 'Function is not serializable'\n : { [K in keyof T]: ValidateJSON<T[K]> }\n\nexport type LooseReturnType<T> = T extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn\n : never\n\nexport type LooseAsyncReturnType<T> = T extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn extends Promise<infer TReturn>\n ? TReturn\n : TReturn\n : never\n\n/**\n * Return the last element of an array.\n * Intended for non-empty arrays used within router internals.\n */\nexport function last<T>(arr: ReadonlyArray<T>) {\n return arr[arr.length - 1]\n}\n\nfunction isFunction(d: any): d is Function {\n return typeof d === 'function'\n}\n\n/**\n * Apply a value-or-updater to a previous value.\n * Accepts either a literal value or a function of the previous value.\n */\nexport function functionalUpdate<TPrevious, TResult = TPrevious>(\n updater: Updater<TPrevious, TResult> | NonNullableUpdater<TPrevious, TResult>,\n previous: TPrevious,\n): TResult {\n if (isFunction(updater)) {\n return updater(previous)\n }\n\n return updater\n}\n\nconst hasOwn = Object.prototype.hasOwnProperty\nconst isEnumerable = Object.prototype.propertyIsEnumerable\n\n/**\n * This function returns `prev` if `_next` is deeply equal.\n * If not, it will replace any deeply equal children of `b` with those of `a`.\n * This can be used for structural sharing between immutable JSON values for example.\n * Do not use this with signals\n */\nexport function replaceEqualDeep<T>(prev: any, _next: T, _depth = 0): T {\n if (isServer) {\n return _next\n }\n if (prev === _next) {\n return prev\n }\n\n if (_depth > 500) return _next\n\n const next = _next as any\n\n const array = isPlainArray(prev) && isPlainArray(next)\n\n if (!array && !(isPlainObject(prev) && isPlainObject(next))) return next\n\n const prevItems = array ? prev : getEnumerableOwnKeys(prev)\n if (!prevItems) return next\n const nextItems = array ? next : getEnumerableOwnKeys(next)\n if (!nextItems) return next\n const prevSize = prevItems.length\n const nextSize = nextItems.length\n const copy: any = array ? new Array(nextSize) : {}\n\n let equalItems = 0\n\n for (let i = 0; i < nextSize; i++) {\n const key = array ? i : (nextItems[i] as any)\n const p = prev[key]\n const n = next[key]\n\n if (p === n) {\n copy[key] = p\n if (array ? i < prevSize : hasOwn.call(prev, key)) equalItems++\n continue\n }\n\n if (\n p === null ||\n n === null ||\n typeof p !== 'object' ||\n typeof n !== 'object'\n ) {\n copy[key] = n\n continue\n }\n\n const v = replaceEqualDeep(p, n, _depth + 1)\n copy[key] = v\n if (v === p) equalItems++\n }\n\n return prevSize === nextSize && equalItems === prevSize ? prev : copy\n}\n\n/**\n * Equivalent to `Reflect.ownKeys`, but ensures that objects are \"clone-friendly\":\n * will return false if object has any non-enumerable properties.\n *\n * Optimized for the common case where objects have no symbol properties.\n */\nfunction getEnumerableOwnKeys(o: object) {\n const names = Object.getOwnPropertyNames(o)\n\n // Fast path: check all string property names are enumerable\n for (const name of names) {\n if (!isEnumerable.call(o, name)) return false\n }\n\n // Only check symbols if the object has any (most plain objects don't)\n const symbols = Object.getOwnPropertySymbols(o)\n\n // Fast path: no symbols, return names directly (avoids array allocation/concat)\n if (symbols.length === 0) return names\n\n // Slow path: has symbols, need to check and merge\n const keys: Array<string | symbol> = names\n for (const symbol of symbols) {\n if (!isEnumerable.call(o, symbol)) return false\n keys.push(symbol)\n }\n return keys\n}\n\n// Copied from: https://github.com/jonschlinkert/is-plain-object\nexport function isPlainObject(o: any) {\n if (!hasObjectPrototype(o)) {\n return false\n }\n\n // If has modified constructor\n const ctor = o.constructor\n if (typeof ctor === 'undefined') {\n return true\n }\n\n // If has modified prototype\n const prot = ctor.prototype\n if (!hasObjectPrototype(prot)) {\n return false\n }\n\n // If constructor does not have an Object-specific method\n if (!prot.hasOwnProperty('isPrototypeOf')) {\n return false\n }\n\n // Most likely a plain Object\n return true\n}\n\nfunction hasObjectPrototype(o: any) {\n return Object.prototype.toString.call(o) === '[object Object]'\n}\n\n/**\n * Check if a value is a \"plain\" array (no extra enumerable keys).\n */\nexport function isPlainArray(value: unknown): value is Array<unknown> {\n return Array.isArray(value) && value.length === Object.keys(value).length\n}\n\n/**\n * Perform a deep equality check with options for partial comparison and\n * ignoring `undefined` values. Optimized for router state comparisons.\n */\nexport function deepEqual(\n a: any,\n b: any,\n opts?: { partial?: boolean; ignoreUndefined?: boolean },\n): boolean {\n if (a === b) {\n return true\n }\n\n if (typeof a !== typeof b) {\n return false\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let i = 0, l = a.length; i < l; i++) {\n if (!deepEqual(a[i], b[i], opts)) return false\n }\n return true\n }\n\n if (isPlainObject(a) && isPlainObject(b)) {\n const ignoreUndefined = opts?.ignoreUndefined ?? true\n\n if (opts?.partial) {\n for (const k in b) {\n if (!ignoreUndefined || b[k] !== undefined) {\n if (!deepEqual(a[k], b[k], opts)) return false\n }\n }\n return true\n }\n\n let aCount = 0\n if (!ignoreUndefined) {\n aCount = Object.keys(a).length\n } else {\n for (const k in a) {\n if (a[k] !== undefined) aCount++\n }\n }\n\n let bCount = 0\n for (const k in b) {\n if (!ignoreUndefined || b[k] !== undefined) {\n bCount++\n if (bCount > aCount || !deepEqual(a[k], b[k], opts)) return false\n }\n }\n\n return aCount === bCount\n }\n\n return false\n}\n\nexport type StringLiteral<T> = T extends string\n ? string extends T\n ? string\n : T\n : never\n\nexport type ThrowOrOptional<T, TThrow extends boolean> = TThrow extends true\n ? T\n : T | undefined\n\nexport type StrictOrFrom<\n TRouter extends AnyRouter,\n TFrom,\n TStrict extends boolean = true,\n> = TStrict extends false\n ? {\n from?: never\n strict: TStrict\n }\n : {\n from: ConstrainLiteral<TFrom, RouteIds<TRouter['routeTree']>>\n strict?: TStrict\n }\n\nexport type ThrowConstraint<\n TStrict extends boolean,\n TThrow extends boolean,\n> = TStrict extends false ? (TThrow extends true ? never : TThrow) : TThrow\n\nexport type ControlledPromise<T> = Promise<T> & {\n resolve: (value: T) => void\n reject: (value: any) => void\n status: 'pending' | 'resolved' | 'rejected'\n value?: T\n}\n\n/**\n * Create a promise with exposed resolve/reject and status fields.\n * Useful for coordinating async router lifecycle operations.\n */\nexport function createControlledPromise<T>(onResolve?: (value: T) => void) {\n let resolveLoadPromise!: (value: T) => void\n let rejectLoadPromise!: (value: any) => void\n\n const controlledPromise = new Promise<T>((resolve, reject) => {\n resolveLoadPromise = resolve\n rejectLoadPromise = reject\n }) as ControlledPromise<T>\n\n controlledPromise.status = 'pending'\n\n controlledPromise.resolve = (value: T) => {\n controlledPromise.status = 'resolved'\n controlledPromise.value = value\n resolveLoadPromise(value)\n onResolve?.(value)\n }\n\n controlledPromise.reject = (e) => {\n controlledPromise.status = 'rejected'\n rejectLoadPromise(e)\n }\n\n return controlledPromise\n}\n\n/**\n * Heuristically detect dynamic import \"module not found\" errors\n * across major browsers for lazy route component handling.\n */\nexport function isModuleNotFoundError(error: any): boolean {\n // chrome: \"Failed to fetch dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split\"\n // firefox: \"error loading dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split\"\n // safari: \"Importing a module script failed.\"\n if (typeof error?.message !== 'string') return false\n return (\n error.message.startsWith('Failed to fetch dynamically imported module') ||\n error.message.startsWith('error loading dynamically imported module') ||\n error.message.startsWith('Importing a module script failed')\n )\n}\n\nexport function isPromise<T>(\n value: Promise<Awaited<T>> | T,\n): value is Promise<Awaited<T>> {\n return Boolean(\n value &&\n typeof value === 'object' &&\n typeof (value as Promise<T>).then === 'function',\n )\n}\n\nexport function findLast<T>(\n array: ReadonlyArray<T>,\n predicate: (item: T) => boolean,\n): T | undefined {\n for (let i = array.length - 1; i >= 0; i--) {\n const item = array[i]!\n if (predicate(item)) return item\n }\n return undefined\n}\n\n/**\n * Remove control characters that can cause open redirect vulnerabilities.\n * Characters like \\r (CR) and \\n (LF) can trick URL parsers into interpreting\n * paths like \"/\\r/evil.com\" as \"http://evil.com\".\n */\nfunction sanitizePathSegment(segment: string): string {\n // Remove ASCII control characters (0x00-0x1F) and DEL (0x7F)\n // These include CR (\\r = 0x0D), LF (\\n = 0x0A), and other potentially dangerous characters\n // eslint-disable-next-line no-control-regex\n return segment.replace(/[\\x00-\\x1f\\x7f]/g, '')\n}\n\nfunction decodeSegment(segment: string): string {\n let decoded: string\n try {\n decoded = decodeURI(segment)\n } catch {\n // if the decoding fails, try to decode the various parts leaving the malformed tags in place\n decoded = segment.replaceAll(/%[0-9A-F]{2}/gi, (match) => {\n try {\n return decodeURI(match)\n } catch {\n return match\n }\n })\n }\n return sanitizePathSegment(decoded)\n}\n\n/**\n * List of URL protocols that are safe for navigation.\n * Only these protocols are allowed in redirects and navigation.\n */\nexport const SAFE_URL_PROTOCOLS = ['http:', 'https:', 'mailto:', 'tel:']\n\n/**\n * Check if a URL string uses a protocol that is not in the safe list.\n * Returns true for dangerous protocols like javascript:, data:, vbscript:, etc.\n *\n * The URL constructor correctly normalizes:\n * - Mixed case (JavaScript: → javascript:)\n * - Whitespace/control characters (java\\nscript: → javascript:)\n * - Leading whitespace\n *\n * For relative URLs (no protocol), returns false (safe).\n *\n * @param url - The URL string to check\n * @returns true if the URL uses a dangerous (non-whitelisted) protocol\n */\nexport function isDangerousProtocol(url: string): boolean {\n if (!url) return false\n\n try {\n // Use the URL constructor - it correctly normalizes protocols\n // per WHATWG URL spec, handling all bypass attempts automatically\n const parsed = new URL(url)\n return !SAFE_URL_PROTOCOLS.includes(parsed.protocol)\n } catch {\n // URL constructor throws for relative URLs (no protocol)\n // These are safe - they can't execute scripts\n return false\n }\n}\n\n// This utility is based on https://github.com/zertosh/htmlescape\n// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE\nconst HTML_ESCAPE_LOOKUP: { [match: string]: string } = {\n '&': '\\\\u0026',\n '>': '\\\\u003e',\n '<': '\\\\u003c',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n}\n\nconst HTML_ESCAPE_REGEX = /[&><\\u2028\\u2029]/g\n\n/**\n * Escape HTML special characters in a string to prevent XSS attacks\n * when embedding strings in script tags during SSR.\n *\n * This is essential for preventing XSS vulnerabilities when user-controlled\n * content is embedded in inline scripts.\n */\nexport function escapeHtml(str: string): string {\n return str.replace(HTML_ESCAPE_REGEX, (match) => HTML_ESCAPE_LOOKUP[match]!)\n}\n\nexport function decodePath(path: string, decodeIgnore?: Array<string>): string {\n if (!path) return path\n const re = decodeIgnore\n ? new RegExp(`${decodeIgnore.join('|')}`, 'gi')\n : /%25|%5C/gi\n let cursor = 0\n let result = ''\n let match\n while (null !== (match = re.exec(path))) {\n result += decodeSegment(path.slice(cursor, match.index)) + match[0]\n cursor = re.lastIndex\n }\n result = result + decodeSegment(cursor ? path.slice(cursor) : path)\n\n // Prevent open redirect via protocol-relative URLs (e.g. \"//evil.com\")\n // After sanitizing control characters, paths like \"/\\r/evil.com\" become \"//evil.com\"\n // Collapse leading double slashes to a single slash\n if (result.startsWith('//')) {\n result = '/' + result.replace(/^\\/+/, '')\n }\n\n return result\n}\n\n/**\n * Encodes non-ASCII (unicode) characters in a path while preserving\n * already percent-encoded sequences. This is used to generate proper\n * href values without constructing URL objects.\n *\n * Unlike encodeURI, this won't double-encode percent-encoded sequences\n * like %2F or %25 because it only targets non-ASCII characters.\n */\nexport function encodeNonAscii(path: string): string {\n // eslint-disable-next-line no-control-regex\n if (!/[^\\u0000-\\u007F]/.test(path)) return path\n // eslint-disable-next-line no-control-regex\n return path.replace(/[^\\u0000-\\u007F]/gu, encodeURIComponent)\n}\n\n/**\n * Builds the dev-mode CSS styles URL for route-scoped CSS collection.\n * Used by HeadContent components in all framework implementations to construct\n * the URL for the `/@tanstack-start/styles.css` endpoint.\n *\n * @param basepath - The router's basepath (may or may not have leading slash)\n * @param routeIds - Array of matched route IDs to include in the CSS collection\n * @returns The full URL path for the dev styles CSS endpoint\n */\nexport function buildDevStylesUrl(\n basepath: string,\n routeIds: Array<string>,\n): string {\n // Trim all leading and trailing slashes from basepath\n const trimmedBasepath = basepath.replace(/^\\/+|\\/+$/g, '')\n // Build normalized basepath: empty string for root, or '/path' for non-root\n const normalizedBasepath = trimmedBasepath === '' ? '' : `/${trimmedBasepath}`\n return `${normalizedBasepath}/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds.join(','))}`\n}\n"],"names":[],"mappings":";AA+LO,SAAS,KAAQ,KAAuB;AAC7C,SAAO,IAAI,IAAI,SAAS,CAAC;AAC3B;AAEA,SAAS,WAAW,GAAuB;AACzC,SAAO,OAAO,MAAM;AACtB;AAMO,SAAS,iBACd,SACA,UACS;AACT,MAAI,WAAW,OAAO,GAAG;AACvB,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAEA,SAAO;AACT;AAEA,MAAM,SAAS,OAAO,UAAU;AAChC,MAAM,eAAe,OAAO,UAAU;AAQ/B,SAAS,iBAAoB,MAAW,OAAU,SAAS,GAAM;AACtE,MAAI,UAAU;AACZ,WAAO;AAAA,EACT;AACA,MAAI,SAAS,OAAO;AAClB,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,IAAK,QAAO;AAEzB,QAAM,OAAO;AAEb,QAAM,QAAQ,aAAa,IAAI,KAAK,aAAa,IAAI;AAErD,MAAI,CAAC,SAAS,EAAE,cAAc,IAAI,KAAK,cAAc,IAAI,GAAI,QAAO;AAEpE,QAAM,YAAY,QAAQ,OAAO,qBAAqB,IAAI;AAC1D,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,YAAY,QAAQ,OAAO,qBAAqB,IAAI;AAC1D,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,WAAW,UAAU;AAC3B,QAAM,WAAW,UAAU;AAC3B,QAAM,OAAY,QAAQ,IAAI,MAAM,QAAQ,IAAI,CAAA;AAEhD,MAAI,aAAa;AAEjB,WAAS,IAAI,GAAG,IAAI,UAAU,KAAK;AACjC,UAAM,MAAM,QAAQ,IAAK,UAAU,CAAC;AACpC,UAAM,IAAI,KAAK,GAAG;AAClB,UAAM,IAAI,KAAK,GAAG;AAElB,QAAI,MAAM,GAAG;AACX,WAAK,GAAG,IAAI;AACZ,UAAI,QAAQ,IAAI,WAAW,OAAO,KAAK,MAAM,GAAG,EAAG;AACnD;AAAA,IACF;AAEA,QACE,MAAM,QACN,MAAM,QACN,OAAO,MAAM,YACb,OAAO,MAAM,UACb;AACA,WAAK,GAAG,IAAI;AACZ;AAAA,IACF;AAEA,UAAM,IAAI,iBAAiB,GAAG,GAAG,SAAS,CAAC;AAC3C,SAAK,GAAG,IAAI;AACZ,QAAI,MAAM,EAAG;AAAA,EACf;AAEA,SAAO,aAAa,YAAY,eAAe,WAAW,OAAO;AACnE;AAQA,SAAS,qBAAqB,GAAW;AACvC,QAAM,QAAQ,OAAO,oBAAoB,CAAC;AAG1C,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,aAAa,KAAK,GAAG,IAAI,EAAG,QAAO;AAAA,EAC1C;AAGA,QAAM,UAAU,OAAO,sBAAsB,CAAC;AAG9C,MAAI,QAAQ,WAAW,EAAG,QAAO;AAGjC,QAAM,OAA+B;AACrC,aAAW,UAAU,SAAS;AAC5B,QAAI,CAAC,aAAa,KAAK,GAAG,MAAM,EAAG,QAAO;AAC1C,SAAK,KAAK,MAAM;AAAA,EAClB;AACA,SAAO;AACT;AAGO,SAAS,cAAc,GAAQ;AACpC,MAAI,CAAC,mBAAmB,CAAC,GAAG;AAC1B,WAAO;AAAA,EACT;AAGA,QAAM,OAAO,EAAE;AACf,MAAI,OAAO,SAAS,aAAa;AAC/B,WAAO;AAAA,EACT;AAGA,QAAM,OAAO,KAAK;AAClB,MAAI,CAAC,mBAAmB,IAAI,GAAG;AAC7B,WAAO;AAAA,EACT;AAGA,MAAI,CAAC,KAAK,eAAe,eAAe,GAAG;AACzC,WAAO;AAAA,EACT;AAGA,SAAO;AACT;AAEA,SAAS,mBAAmB,GAAQ;AAClC,SAAO,OAAO,UAAU,SAAS,KAAK,CAAC,MAAM;AAC/C;AAKO,SAAS,aAAa,OAAyC;AACpE,SAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,OAAO,KAAK,KAAK,EAAE;AACrE;AAMO,SAAS,UACd,GACA,GACA,MACS;AACT,MAAI,MAAM,GAAG;AACX,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,MAAM,OAAO,GAAG;AACzB,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAAG;AACxC,QAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,aAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAI,GAAG,KAAK;AACxC,UAAI,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,EAAG,QAAO;AAAA,IAC3C;AACA,WAAO;AAAA,EACT;AAEA,MAAI,cAAc,CAAC,KAAK,cAAc,CAAC,GAAG;AACxC,UAAM,kBAAkB,MAAM,mBAAmB;AAEjD,QAAI,MAAM,SAAS;AACjB,iBAAW,KAAK,GAAG;AACjB,YAAI,CAAC,mBAAmB,EAAE,CAAC,MAAM,QAAW;AAC1C,cAAI,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,EAAG,QAAO;AAAA,QAC3C;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAEA,QAAI,SAAS;AACb,QAAI,CAAC,iBAAiB;AACpB,eAAS,OAAO,KAAK,CAAC,EAAE;AAAA,IAC1B,OAAO;AACL,iBAAW,KAAK,GAAG;AACjB,YAAI,EAAE,CAAC,MAAM,OAAW;AAAA,MAC1B;AAAA,IACF;AAEA,QAAI,SAAS;AACb,eAAW,KAAK,GAAG;AACjB,UAAI,CAAC,mBAAmB,EAAE,CAAC,MAAM,QAAW;AAC1C;AACA,YAAI,SAAS,UAAU,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,EAAG,QAAO;AAAA,MAC9D;AAAA,IACF;AAEA,WAAO,WAAW;AAAA,EACpB;AAEA,SAAO;AACT;AA0CO,SAAS,wBAA2B,WAAgC;AACzE,MAAI;AACJ,MAAI;AAEJ,QAAM,oBAAoB,IAAI,QAAW,CAAC,SAAS,WAAW;AAC5D,yBAAqB;AACrB,wBAAoB;AAAA,EACtB,CAAC;AAED,oBAAkB,SAAS;AAE3B,oBAAkB,UAAU,CAAC,UAAa;AACxC,sBAAkB,SAAS;AAC3B,sBAAkB,QAAQ;AAC1B,uBAAmB,KAAK;AACxB,gBAAY,KAAK;AAAA,EACnB;AAEA,oBAAkB,SAAS,CAAC,MAAM;AAChC,sBAAkB,SAAS;AAC3B,sBAAkB,CAAC;AAAA,EACrB;AAEA,SAAO;AACT;AAMO,SAAS,sBAAsB,OAAqB;AAIzD,MAAI,OAAO,OAAO,YAAY,SAAU,QAAO;AAC/C,SACE,MAAM,QAAQ,WAAW,6CAA6C,KACtE,MAAM,QAAQ,WAAW,2CAA2C,KACpE,MAAM,QAAQ,WAAW,kCAAkC;AAE/D;AAEO,SAAS,UACd,OAC8B;AAC9B,SAAO;AAAA,IACL,SACA,OAAO,UAAU,YACjB,OAAQ,MAAqB,SAAS;AAAA,EAAA;AAE1C;AAEO,SAAS,SACd,OACA,WACe;AACf,WAAS,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;AAC1C,UAAM,OAAO,MAAM,CAAC;AACpB,QAAI,UAAU,IAAI,EAAG,QAAO;AAAA,EAC9B;AACA,SAAO;AACT;AAOA,SAAS,oBAAoB,SAAyB;AAIpD,SAAO,QAAQ,QAAQ,oBAAoB,EAAE;AAC/C;AAEA,SAAS,cAAc,SAAyB;AAC9C,MAAI;AACJ,MAAI;AACF,cAAU,UAAU,OAAO;AAAA,EAC7B,QAAQ;AAEN,cAAU,QAAQ,WAAW,kBAAkB,CAAC,UAAU;AACxD,UAAI;AACF,eAAO,UAAU,KAAK;AAAA,MACxB,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO,oBAAoB,OAAO;AACpC;AAMO,MAAM,qBAAqB,CAAC,SAAS,UAAU,WAAW,MAAM;AAgBhE,SAAS,oBAAoB,KAAsB;AACxD,MAAI,CAAC,IAAK,QAAO;AAEjB,MAAI;AAGF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,WAAO,CAAC,mBAAmB,SAAS,OAAO,QAAQ;AAAA,EACrD,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;AAIA,MAAM,qBAAkD;AAAA,EACtD,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,UAAU;AAAA,EACV,UAAU;AACZ;AAEA,MAAM,oBAAoB;AASnB,SAAS,WAAW,KAAqB;AAC9C,SAAO,IAAI,QAAQ,mBAAmB,CAAC,UAAU,mBAAmB,KAAK,CAAE;AAC7E;AAEO,SAAS,WAAW,MAAc,cAAsC;AAC7E,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,KAAK,eACP,IAAI,OAAO,GAAG,aAAa,KAAK,GAAG,CAAC,IAAI,IAAI,IAC5C;AACJ,MAAI,SAAS;AACb,MAAI,SAAS;AACb,MAAI;AACJ,SAAO,UAAU,QAAQ,GAAG,KAAK,IAAI,IAAI;AACvC,cAAU,cAAc,KAAK,MAAM,QAAQ,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC;AAClE,aAAS,GAAG;AAAA,EACd;AACA,WAAS,SAAS,cAAc,SAAS,KAAK,MAAM,MAAM,IAAI,IAAI;AAKlE,MAAI,OAAO,WAAW,IAAI,GAAG;AAC3B,aAAS,MAAM,OAAO,QAAQ,QAAQ,EAAE;AAAA,EAC1C;AAEA,SAAO;AACT;AAUO,SAAS,eAAe,MAAsB;AAEnD,MAAI,CAAC,mBAAmB,KAAK,IAAI,EAAG,QAAO;AAE3C,SAAO,KAAK,QAAQ,sBAAsB,kBAAkB;AAC9D;AAWO,SAAS,kBACd,UACA,UACQ;AAER,QAAM,kBAAkB,SAAS,QAAQ,cAAc,EAAE;AAEzD,QAAM,qBAAqB,oBAAoB,KAAK,KAAK,IAAI,eAAe;AAC5E,SAAO,GAAG,kBAAkB,sCAAsC,mBAAmB,SAAS,KAAK,GAAG,CAAC,CAAC;AAC1G;"}
|
package/package.json
CHANGED
package/src/load-matches.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { batch } from '@tanstack/store'
|
|
2
1
|
import invariant from 'tiny-invariant'
|
|
3
2
|
import { isServer } from '@tanstack/router-core/isServer'
|
|
3
|
+
import { batch } from './utils/batch'
|
|
4
4
|
import { createControlledPromise, isPromise } from './utils'
|
|
5
5
|
import { isNotFound } from './not-found'
|
|
6
6
|
import { rootRouteId } from './root'
|
package/src/router.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Store
|
|
1
|
+
import { Store } from '@tanstack/store'
|
|
2
2
|
import { createBrowserHistory, parseHref } from '@tanstack/history'
|
|
3
3
|
import { isServer } from '@tanstack/router-core/isServer'
|
|
4
|
+
import { batch } from './utils/batch'
|
|
4
5
|
import {
|
|
5
6
|
createControlledPromise,
|
|
6
7
|
decodePath,
|
|
@@ -898,6 +899,24 @@ declare global {
|
|
|
898
899
|
*
|
|
899
900
|
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/RouterType
|
|
900
901
|
*/
|
|
902
|
+
type RouterStateStore<TState> = {
|
|
903
|
+
state: TState
|
|
904
|
+
setState: (updater: (prev: TState) => TState) => void
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function createServerStore<TState>(
|
|
908
|
+
initialState: TState,
|
|
909
|
+
): RouterStateStore<TState> {
|
|
910
|
+
const store = {
|
|
911
|
+
state: initialState,
|
|
912
|
+
setState: (updater: (prev: TState) => TState) => {
|
|
913
|
+
store.state = updater(store.state)
|
|
914
|
+
},
|
|
915
|
+
} as RouterStateStore<TState>
|
|
916
|
+
|
|
917
|
+
return store
|
|
918
|
+
}
|
|
919
|
+
|
|
901
920
|
export class RouterCore<
|
|
902
921
|
in out TRouteTree extends AnyRoute,
|
|
903
922
|
in out TTrailingSlashOption extends TrailingSlashOption,
|
|
@@ -972,8 +991,8 @@ export class RouterCore<
|
|
|
972
991
|
}
|
|
973
992
|
}
|
|
974
993
|
|
|
975
|
-
//
|
|
976
|
-
// by the router provider once rendered. We provide
|
|
994
|
+
// This is a default implementation that can optionally be overridden
|
|
995
|
+
// by the router provider once rendered. We provide this so that the
|
|
977
996
|
// router can be used in a non-react environment if necessary
|
|
978
997
|
startTransition: StartTransitionFn = (fn) => fn()
|
|
979
998
|
|
|
@@ -1076,18 +1095,24 @@ export class RouterCore<
|
|
|
1076
1095
|
}
|
|
1077
1096
|
|
|
1078
1097
|
if (!this.__store && this.latestLocation) {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
this.
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1098
|
+
if (isServer ?? this.isServer) {
|
|
1099
|
+
this.__store = createServerStore(
|
|
1100
|
+
getInitialRouterState(this.latestLocation),
|
|
1101
|
+
) as unknown as Store<any>
|
|
1102
|
+
} else {
|
|
1103
|
+
this.__store = new Store(getInitialRouterState(this.latestLocation), {
|
|
1104
|
+
onUpdate: () => {
|
|
1105
|
+
this.__store.state = {
|
|
1106
|
+
...this.state,
|
|
1107
|
+
cachedMatches: this.state.cachedMatches.filter(
|
|
1108
|
+
(d) => !['redirected'].includes(d.status),
|
|
1109
|
+
),
|
|
1110
|
+
}
|
|
1111
|
+
},
|
|
1112
|
+
})
|
|
1089
1113
|
|
|
1090
|
-
|
|
1114
|
+
setupScrollRestoration(this)
|
|
1115
|
+
}
|
|
1091
1116
|
}
|
|
1092
1117
|
|
|
1093
1118
|
let needsLocationUpdate = false
|
package/src/ssr/ssr-client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import invariant from 'tiny-invariant'
|
|
2
|
-
import { batch } from '
|
|
2
|
+
import { batch } from '../utils/batch'
|
|
3
3
|
import { isNotFound } from '../not-found'
|
|
4
4
|
import { createControlledPromise } from '../utils'
|
|
5
5
|
import type { GLOBAL_SEROVAL, GLOBAL_TSR } from './constants'
|
package/src/ssr/ssr-server.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'
|
|
2
2
|
import invariant from 'tiny-invariant'
|
|
3
3
|
import { decodePath } from '../utils'
|
|
4
|
+
import { createLRUCache } from '../lru-cache'
|
|
4
5
|
import minifiedTsrBootStrapScript from './tsrScript?script-string'
|
|
5
6
|
import { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'
|
|
6
7
|
import { defaultSerovalPlugins } from './serializer/seroval-plugins'
|
|
7
8
|
import { makeSsrSerovalPlugin } from './serializer/transformer'
|
|
9
|
+
import type { LRUCache } from '../lru-cache'
|
|
8
10
|
import type { DehydratedMatch, DehydratedRouter } from './types'
|
|
9
11
|
import type { AnySerializationAdapter } from './serializer/transformer'
|
|
10
12
|
import type { AnyRouter } from '../router'
|
|
@@ -28,6 +30,10 @@ declare module '../router' {
|
|
|
28
30
|
|
|
29
31
|
const SCOPE_ID = 'tsr'
|
|
30
32
|
|
|
33
|
+
const TSR_PREFIX = GLOBAL_TSR + '.router='
|
|
34
|
+
const P_PREFIX = GLOBAL_TSR + '.p(()=>'
|
|
35
|
+
const P_SUFFIX = ')'
|
|
36
|
+
|
|
31
37
|
export function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {
|
|
32
38
|
const dehydratedMatch: DehydratedMatch = {
|
|
33
39
|
i: match.id,
|
|
@@ -116,6 +122,10 @@ class ScriptBuffer {
|
|
|
116
122
|
if (bufferedScripts.length === 0) {
|
|
117
123
|
return undefined
|
|
118
124
|
}
|
|
125
|
+
// Optimization: if only one script, avoid join
|
|
126
|
+
if (bufferedScripts.length === 1) {
|
|
127
|
+
return bufferedScripts[0] + ';document.currentScript.remove()'
|
|
128
|
+
}
|
|
119
129
|
// Append cleanup script and join - avoid push() to not mutate then iterate
|
|
120
130
|
return bufferedScripts.join(';') + ';document.currentScript.remove()'
|
|
121
131
|
}
|
|
@@ -137,6 +147,23 @@ class ScriptBuffer {
|
|
|
137
147
|
}
|
|
138
148
|
}
|
|
139
149
|
|
|
150
|
+
const isProd = process.env.NODE_ENV === 'production'
|
|
151
|
+
|
|
152
|
+
type FilteredRoutes = Manifest['routes']
|
|
153
|
+
|
|
154
|
+
type ManifestLRU = LRUCache<string, FilteredRoutes>
|
|
155
|
+
|
|
156
|
+
const MANIFEST_CACHE_SIZE = 100
|
|
157
|
+
const manifestCaches = new WeakMap<Manifest, ManifestLRU>()
|
|
158
|
+
|
|
159
|
+
function getManifestCache(manifest: Manifest): ManifestLRU {
|
|
160
|
+
const cache = manifestCaches.get(manifest)
|
|
161
|
+
if (cache) return cache
|
|
162
|
+
const newCache = createLRUCache<string, FilteredRoutes>(MANIFEST_CACHE_SIZE)
|
|
163
|
+
manifestCaches.set(manifest, newCache)
|
|
164
|
+
return newCache
|
|
165
|
+
}
|
|
166
|
+
|
|
140
167
|
export function attachRouterServerSsrUtils({
|
|
141
168
|
router,
|
|
142
169
|
manifest,
|
|
@@ -152,13 +179,13 @@ export function attachRouterServerSsrUtils({
|
|
|
152
179
|
const renderFinishedListeners: Array<() => void> = []
|
|
153
180
|
const serializationFinishedListeners: Array<() => void> = []
|
|
154
181
|
const scriptBuffer = new ScriptBuffer(router)
|
|
155
|
-
let injectedHtmlBuffer
|
|
182
|
+
let injectedHtmlBuffer = ''
|
|
156
183
|
|
|
157
184
|
router.serverSsr = {
|
|
158
185
|
injectHtml: (html: string) => {
|
|
159
186
|
if (!html) return
|
|
160
187
|
// Buffer the HTML so it can be retrieved via takeBufferedHtml()
|
|
161
|
-
injectedHtmlBuffer
|
|
188
|
+
injectedHtmlBuffer += html
|
|
162
189
|
// Emit event to notify subscribers that new HTML is available
|
|
163
190
|
router.emit({
|
|
164
191
|
type: 'onInjectedHtml',
|
|
@@ -182,31 +209,41 @@ export function attachRouterServerSsrUtils({
|
|
|
182
209
|
// For currently matched routes, send full manifest (preloads + assets)
|
|
183
210
|
// For all other routes, only send assets (no preloads as they are handled via dynamic imports)
|
|
184
211
|
if (manifest) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
212
|
+
// Prod-only caching; in dev manifests may be replaced/updated (HMR)
|
|
213
|
+
const currentRouteIdsList = matchesToDehydrate.map((m) => m.routeId)
|
|
214
|
+
const manifestCacheKey = currentRouteIdsList.join('\0')
|
|
215
|
+
|
|
216
|
+
let filteredRoutes: FilteredRoutes | undefined
|
|
217
|
+
|
|
218
|
+
if (isProd) {
|
|
219
|
+
filteredRoutes = getManifestCache(manifest).get(manifestCacheKey)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!filteredRoutes) {
|
|
223
|
+
const currentRouteIds = new Set(currentRouteIdsList)
|
|
224
|
+
const nextFilteredRoutes: FilteredRoutes = {}
|
|
225
|
+
|
|
226
|
+
for (const routeId in manifest.routes) {
|
|
227
|
+
const routeManifest = manifest.routes[routeId]!
|
|
228
|
+
if (currentRouteIds.has(routeId)) {
|
|
229
|
+
nextFilteredRoutes[routeId] = routeManifest
|
|
230
|
+
} else if (
|
|
231
|
+
routeManifest.assets &&
|
|
232
|
+
routeManifest.assets.length > 0
|
|
233
|
+
) {
|
|
234
|
+
nextFilteredRoutes[routeId] = {
|
|
235
|
+
assets: routeManifest.assets,
|
|
205
236
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (isProd) {
|
|
241
|
+
getManifestCache(manifest).set(manifestCacheKey, nextFilteredRoutes)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
filteredRoutes = nextFilteredRoutes
|
|
245
|
+
}
|
|
246
|
+
|
|
210
247
|
manifestToDehydrate = {
|
|
211
248
|
routes: filteredRoutes,
|
|
212
249
|
}
|
|
@@ -252,9 +289,9 @@ export function attachRouterServerSsrUtils({
|
|
|
252
289
|
refs: new Map(),
|
|
253
290
|
plugins,
|
|
254
291
|
onSerialize: (data, initial) => {
|
|
255
|
-
let serialized = initial ?
|
|
292
|
+
let serialized = initial ? TSR_PREFIX + data : data
|
|
256
293
|
if (trackPlugins.didRun) {
|
|
257
|
-
serialized =
|
|
294
|
+
serialized = P_PREFIX + serialized + P_SUFFIX
|
|
258
295
|
}
|
|
259
296
|
scriptBuffer.enqueue(serialized)
|
|
260
297
|
},
|
|
@@ -310,11 +347,11 @@ export function attachRouterServerSsrUtils({
|
|
|
310
347
|
scriptBuffer.liftBarrier()
|
|
311
348
|
},
|
|
312
349
|
takeBufferedHtml() {
|
|
313
|
-
if (injectedHtmlBuffer
|
|
350
|
+
if (!injectedHtmlBuffer) {
|
|
314
351
|
return undefined
|
|
315
352
|
}
|
|
316
|
-
const buffered = injectedHtmlBuffer
|
|
317
|
-
injectedHtmlBuffer =
|
|
353
|
+
const buffered = injectedHtmlBuffer
|
|
354
|
+
injectedHtmlBuffer = ''
|
|
318
355
|
return buffered
|
|
319
356
|
},
|
|
320
357
|
cleanup() {
|
|
@@ -322,7 +359,7 @@ export function attachRouterServerSsrUtils({
|
|
|
322
359
|
if (!router.serverSsr) return
|
|
323
360
|
renderFinishedListeners.length = 0
|
|
324
361
|
serializationFinishedListeners.length = 0
|
|
325
|
-
injectedHtmlBuffer =
|
|
362
|
+
injectedHtmlBuffer = ''
|
|
326
363
|
scriptBuffer.cleanup()
|
|
327
364
|
router.serverSsr = undefined
|
|
328
365
|
},
|