@tanstack/router-core 1.157.1 → 1.157.4
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/index.cjs +0 -2
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +0 -1
- package/dist/cjs/load-matches.cjs +8 -8
- package/dist/cjs/load-matches.cjs.map +1 -1
- package/dist/cjs/router.cjs +8 -7
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/scroll-restoration.cjs +3 -2
- package/dist/cjs/scroll-restoration.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
- package/dist/esm/index.d.ts +0 -1
- package/dist/esm/index.js +0 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/load-matches.js +7 -7
- package/dist/esm/load-matches.js.map +1 -1
- package/dist/esm/router.js +8 -7
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/scroll-restoration.js +3 -2
- package/dist/esm/scroll-restoration.js.map +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.js +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +0 -1
- package/src/load-matches.ts +1 -1
- package/src/router.ts +1 -1
- package/src/scroll-restoration.ts +1 -1
- package/src/ssr/transformStreamWithRouter.ts +5 -3
- package/dist/cjs/isServer.d.cts +0 -24
- package/dist/esm/isServer.d.ts +0 -24
- package/src/isServer.ts +0 -24
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isServer } from "@tanstack/router-core/isServer";
|
|
1
2
|
import { functionalUpdate } from "./utils.js";
|
|
2
3
|
function getSafeSessionStorage() {
|
|
3
4
|
try {
|
|
@@ -122,14 +123,14 @@ function restoreScroll({
|
|
|
122
123
|
ignoreScroll = false;
|
|
123
124
|
}
|
|
124
125
|
function setupScrollRestoration(router, force) {
|
|
125
|
-
if (!scrollRestorationCache && !router.isServer) {
|
|
126
|
+
if (!scrollRestorationCache && !(isServer ?? router.isServer)) {
|
|
126
127
|
return;
|
|
127
128
|
}
|
|
128
129
|
const shouldScrollRestoration = force ?? router.options.scrollRestoration ?? false;
|
|
129
130
|
if (shouldScrollRestoration) {
|
|
130
131
|
router.isScrollRestoring = true;
|
|
131
132
|
}
|
|
132
|
-
if (router.isServer || router.isScrollRestorationSetup || !scrollRestorationCache) {
|
|
133
|
+
if ((isServer ?? router.isServer) || router.isScrollRestorationSetup || !scrollRestorationCache) {
|
|
133
134
|
return;
|
|
134
135
|
}
|
|
135
136
|
router.isScrollRestorationSetup = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-restoration.js","sources":["../../src/scroll-restoration.ts"],"sourcesContent":["import { functionalUpdate } from './utils'\nimport { isServer } from './isServer'\nimport type { AnyRouter } from './router'\nimport type { ParsedLocation } from './location'\nimport type { NonNullableUpdater } from './utils'\nimport type { HistoryLocation } from '@tanstack/history'\n\nexport type ScrollRestorationEntry = { scrollX: number; scrollY: number }\n\nexport type ScrollRestorationByElement = Record<string, ScrollRestorationEntry>\n\nexport type ScrollRestorationByKey = Record<string, ScrollRestorationByElement>\n\nexport type ScrollRestorationCache = {\n state: ScrollRestorationByKey\n set: (updater: NonNullableUpdater<ScrollRestorationByKey>) => void\n}\nexport type ScrollRestorationOptions = {\n getKey?: (location: ParsedLocation) => string\n scrollBehavior?: ScrollToOptions['behavior']\n}\n\nfunction getSafeSessionStorage() {\n try {\n if (\n typeof window !== 'undefined' &&\n typeof window.sessionStorage === 'object'\n ) {\n return window.sessionStorage\n }\n } catch {\n // silent\n }\n return undefined\n}\n\n/** SessionStorage key used to persist scroll restoration state. */\n/** SessionStorage key used to store scroll positions across navigations. */\n/** SessionStorage key used to store scroll positions across navigations. */\nexport const storageKey = 'tsr-scroll-restoration-v1_3'\n\nconst throttle = (fn: (...args: Array<any>) => void, wait: number) => {\n let timeout: any\n return (...args: Array<any>) => {\n if (!timeout) {\n timeout = setTimeout(() => {\n fn(...args)\n timeout = null\n }, wait)\n }\n }\n}\n\nfunction createScrollRestorationCache(): ScrollRestorationCache | null {\n const safeSessionStorage = getSafeSessionStorage()\n if (!safeSessionStorage) {\n return null\n }\n\n const persistedState = safeSessionStorage.getItem(storageKey)\n let state: ScrollRestorationByKey = persistedState\n ? JSON.parse(persistedState)\n : {}\n\n return {\n state,\n // This setter is simply to make sure that we set the sessionStorage right\n // after the state is updated. It doesn't necessarily need to be a functional\n // update.\n set: (updater) => {\n state = functionalUpdate(updater, state) || state\n try {\n safeSessionStorage.setItem(storageKey, JSON.stringify(state))\n } catch {\n console.warn(\n '[ts-router] Could not persist scroll restoration state to sessionStorage.',\n )\n }\n },\n }\n}\n\n/** In-memory handle to the persisted scroll restoration cache. */\nexport const scrollRestorationCache = createScrollRestorationCache()\n\n/**\n * The default `getKey` function for `useScrollRestoration`.\n * It returns the `key` from the location state or the `href` of the location.\n *\n * The `location.href` is used as a fallback to support the use case where the location state is not available like the initial render.\n */\n\n/**\n * Default scroll restoration cache key: location state key or full href.\n */\nexport const defaultGetScrollRestorationKey = (location: ParsedLocation) => {\n return location.state.__TSR_key! || location.href\n}\n\n/** Best-effort nth-child CSS selector for a given element. */\nexport function getCssSelector(el: any): string {\n const path = []\n let parent: HTMLElement\n while ((parent = el.parentNode)) {\n path.push(\n `${el.tagName}:nth-child(${Array.prototype.indexOf.call(parent.children, el) + 1})`,\n )\n el = parent\n }\n return `${path.reverse().join(' > ')}`.toLowerCase()\n}\n\nlet ignoreScroll = false\n\n// NOTE: This function must remain pure and not use any outside variables\n// unless they are passed in as arguments. Why? Because we need to be able to\n// toString() it into a script tag to execute as early as possible in the browser\n// during SSR. Additionally, we also call it from within the router lifecycle\nexport function restoreScroll({\n storageKey,\n key,\n behavior,\n shouldScrollRestoration,\n scrollToTopSelectors,\n location,\n}: {\n storageKey: string\n key?: string\n behavior?: ScrollToOptions['behavior']\n shouldScrollRestoration?: boolean\n scrollToTopSelectors?: Array<string | (() => Element | null | undefined)>\n location?: HistoryLocation\n}) {\n let byKey: ScrollRestorationByKey\n\n try {\n byKey = JSON.parse(sessionStorage.getItem(storageKey) || '{}')\n } catch (error) {\n console.error(error)\n return\n }\n\n const resolvedKey = key || window.history.state?.__TSR_key\n const elementEntries = byKey[resolvedKey]\n\n //\n ignoreScroll = true\n\n //\n scroll: {\n // If we have a cached entry for this location state,\n // we always need to prefer that over the hash scroll.\n if (\n shouldScrollRestoration &&\n elementEntries &&\n Object.keys(elementEntries).length > 0\n ) {\n for (const elementSelector in elementEntries) {\n const entry = elementEntries[elementSelector]!\n if (elementSelector === 'window') {\n window.scrollTo({\n top: entry.scrollY,\n left: entry.scrollX,\n behavior,\n })\n } else if (elementSelector) {\n const element = document.querySelector(elementSelector)\n if (element) {\n element.scrollLeft = entry.scrollX\n element.scrollTop = entry.scrollY\n }\n }\n }\n\n break scroll\n }\n\n // If we don't have a cached entry for the hash,\n // Which means we've never seen this location before,\n // we need to check if there is a hash in the URL.\n // If there is, we need to scroll it's ID into view.\n const hash = (location ?? window.location).hash.split('#', 2)[1]\n\n if (hash) {\n const hashScrollIntoViewOptions =\n window.history.state?.__hashScrollIntoViewOptions ?? true\n\n if (hashScrollIntoViewOptions) {\n const el = document.getElementById(hash)\n if (el) {\n el.scrollIntoView(hashScrollIntoViewOptions)\n }\n }\n\n break scroll\n }\n\n // If there is no cached entry for the hash and there is no hash in the URL,\n // we need to scroll to the top of the page for every scrollToTop element\n const scrollOptions = { top: 0, left: 0, behavior }\n window.scrollTo(scrollOptions)\n if (scrollToTopSelectors) {\n for (const selector of scrollToTopSelectors) {\n if (selector === 'window') continue\n const element =\n typeof selector === 'function'\n ? selector()\n : document.querySelector(selector)\n if (element) element.scrollTo(scrollOptions)\n }\n }\n }\n\n //\n ignoreScroll = false\n}\n\n/** Setup global listeners and hooks to support scroll restoration. */\n/** Setup global listeners and hooks to support scroll restoration. */\nexport function setupScrollRestoration(router: AnyRouter, force?: boolean) {\n if (!scrollRestorationCache && !(isServer ?? router.isServer)) {\n return\n }\n const shouldScrollRestoration =\n force ?? router.options.scrollRestoration ?? false\n\n if (shouldScrollRestoration) {\n router.isScrollRestoring = true\n }\n\n if (\n (isServer ?? router.isServer) ||\n router.isScrollRestorationSetup ||\n !scrollRestorationCache\n ) {\n return\n }\n\n router.isScrollRestorationSetup = true\n\n //\n ignoreScroll = false\n\n const getKey =\n router.options.getScrollRestorationKey || defaultGetScrollRestorationKey\n\n window.history.scrollRestoration = 'manual'\n\n // // Create a MutationObserver to monitor DOM changes\n // const mutationObserver = new MutationObserver(() => {\n // ;ignoreScroll = true\n // requestAnimationFrame(() => {\n // ;ignoreScroll = false\n\n // // Attempt to restore scroll position on each dom\n // // mutation until the user scrolls. We do this\n // // because dynamic content may come in at different\n // // ticks after the initial render and we want to\n // // keep up with that content as much as possible.\n // // As soon as the user scrolls, we no longer need\n // // to attempt router.\n // // console.log('mutation observer restoreScroll')\n // restoreScroll(\n // storageKey,\n // getKey(router.state.location),\n // router.options.scrollRestorationBehavior,\n // )\n // })\n // })\n\n // const observeDom = () => {\n // // Observe changes to the entire document\n // mutationObserver.observe(document, {\n // childList: true, // Detect added or removed child nodes\n // subtree: true, // Monitor all descendants\n // characterData: true, // Detect text content changes\n // })\n // }\n\n // const unobserveDom = () => {\n // mutationObserver.disconnect()\n // }\n\n // observeDom()\n\n const onScroll = (event: Event) => {\n // unobserveDom()\n\n if (ignoreScroll || !router.isScrollRestoring) {\n return\n }\n\n let elementSelector = ''\n\n if (event.target === document || event.target === window) {\n elementSelector = 'window'\n } else {\n const attrId = (event.target as Element).getAttribute(\n 'data-scroll-restoration-id',\n )\n\n if (attrId) {\n elementSelector = `[data-scroll-restoration-id=\"${attrId}\"]`\n } else {\n elementSelector = getCssSelector(event.target)\n }\n }\n\n const restoreKey = getKey(router.state.location)\n\n scrollRestorationCache.set((state) => {\n const keyEntry = (state[restoreKey] ||= {} as ScrollRestorationByElement)\n\n const elementEntry = (keyEntry[elementSelector] ||=\n {} as ScrollRestorationEntry)\n\n if (elementSelector === 'window') {\n elementEntry.scrollX = window.scrollX || 0\n elementEntry.scrollY = window.scrollY || 0\n } else if (elementSelector) {\n const element = document.querySelector(elementSelector)\n if (element) {\n elementEntry.scrollX = element.scrollLeft || 0\n elementEntry.scrollY = element.scrollTop || 0\n }\n }\n\n return state\n })\n }\n\n // Throttle the scroll event to avoid excessive updates\n if (typeof document !== 'undefined') {\n document.addEventListener('scroll', throttle(onScroll, 100), true)\n }\n\n router.subscribe('onRendered', (event) => {\n // unobserveDom()\n\n const cacheKey = getKey(event.toLocation)\n\n // If the user doesn't want to restore the scroll position,\n // we don't need to do anything.\n if (!router.resetNextScroll) {\n router.resetNextScroll = true\n return\n }\n if (typeof router.options.scrollRestoration === 'function') {\n const shouldRestore = router.options.scrollRestoration({\n location: router.latestLocation,\n })\n if (!shouldRestore) {\n return\n }\n }\n\n restoreScroll({\n storageKey,\n key: cacheKey,\n behavior: router.options.scrollRestorationBehavior,\n shouldScrollRestoration: router.isScrollRestoring,\n scrollToTopSelectors: router.options.scrollToTopSelectors,\n location: router.history.location,\n })\n\n if (router.isScrollRestoring) {\n // Mark the location as having been seen\n scrollRestorationCache.set((state) => {\n state[cacheKey] ||= {} as ScrollRestorationByElement\n\n return state\n })\n }\n })\n}\n\n/**\n * @private\n * Handles hash-based scrolling after navigation completes.\n * To be used in framework-specific <Transitioner> components during the onResolved event.\n *\n * Provides hash scrolling for programmatic navigation when default browser handling is prevented.\n * @param router The router instance containing current location and state\n */\n/**\n * @private\n * Handles hash-based scrolling after navigation completes.\n * To be used in framework-specific Transitioners.\n */\nexport function handleHashScroll(router: AnyRouter) {\n if (typeof document !== 'undefined' && (document as any).querySelector) {\n const hashScrollIntoViewOptions =\n router.state.location.state.__hashScrollIntoViewOptions ?? true\n\n if (hashScrollIntoViewOptions && router.state.location.hash !== '') {\n const el = document.getElementById(router.state.location.hash)\n if (el) {\n el.scrollIntoView(hashScrollIntoViewOptions)\n }\n }\n }\n}\n"],"names":["storageKey"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scroll-restoration.js","sources":["../../src/scroll-restoration.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport { functionalUpdate } from './utils'\nimport type { AnyRouter } from './router'\nimport type { ParsedLocation } from './location'\nimport type { NonNullableUpdater } from './utils'\nimport type { HistoryLocation } from '@tanstack/history'\n\nexport type ScrollRestorationEntry = { scrollX: number; scrollY: number }\n\nexport type ScrollRestorationByElement = Record<string, ScrollRestorationEntry>\n\nexport type ScrollRestorationByKey = Record<string, ScrollRestorationByElement>\n\nexport type ScrollRestorationCache = {\n state: ScrollRestorationByKey\n set: (updater: NonNullableUpdater<ScrollRestorationByKey>) => void\n}\nexport type ScrollRestorationOptions = {\n getKey?: (location: ParsedLocation) => string\n scrollBehavior?: ScrollToOptions['behavior']\n}\n\nfunction getSafeSessionStorage() {\n try {\n if (\n typeof window !== 'undefined' &&\n typeof window.sessionStorage === 'object'\n ) {\n return window.sessionStorage\n }\n } catch {\n // silent\n }\n return undefined\n}\n\n/** SessionStorage key used to persist scroll restoration state. */\n/** SessionStorage key used to store scroll positions across navigations. */\n/** SessionStorage key used to store scroll positions across navigations. */\nexport const storageKey = 'tsr-scroll-restoration-v1_3'\n\nconst throttle = (fn: (...args: Array<any>) => void, wait: number) => {\n let timeout: any\n return (...args: Array<any>) => {\n if (!timeout) {\n timeout = setTimeout(() => {\n fn(...args)\n timeout = null\n }, wait)\n }\n }\n}\n\nfunction createScrollRestorationCache(): ScrollRestorationCache | null {\n const safeSessionStorage = getSafeSessionStorage()\n if (!safeSessionStorage) {\n return null\n }\n\n const persistedState = safeSessionStorage.getItem(storageKey)\n let state: ScrollRestorationByKey = persistedState\n ? JSON.parse(persistedState)\n : {}\n\n return {\n state,\n // This setter is simply to make sure that we set the sessionStorage right\n // after the state is updated. It doesn't necessarily need to be a functional\n // update.\n set: (updater) => {\n state = functionalUpdate(updater, state) || state\n try {\n safeSessionStorage.setItem(storageKey, JSON.stringify(state))\n } catch {\n console.warn(\n '[ts-router] Could not persist scroll restoration state to sessionStorage.',\n )\n }\n },\n }\n}\n\n/** In-memory handle to the persisted scroll restoration cache. */\nexport const scrollRestorationCache = createScrollRestorationCache()\n\n/**\n * The default `getKey` function for `useScrollRestoration`.\n * It returns the `key` from the location state or the `href` of the location.\n *\n * The `location.href` is used as a fallback to support the use case where the location state is not available like the initial render.\n */\n\n/**\n * Default scroll restoration cache key: location state key or full href.\n */\nexport const defaultGetScrollRestorationKey = (location: ParsedLocation) => {\n return location.state.__TSR_key! || location.href\n}\n\n/** Best-effort nth-child CSS selector for a given element. */\nexport function getCssSelector(el: any): string {\n const path = []\n let parent: HTMLElement\n while ((parent = el.parentNode)) {\n path.push(\n `${el.tagName}:nth-child(${Array.prototype.indexOf.call(parent.children, el) + 1})`,\n )\n el = parent\n }\n return `${path.reverse().join(' > ')}`.toLowerCase()\n}\n\nlet ignoreScroll = false\n\n// NOTE: This function must remain pure and not use any outside variables\n// unless they are passed in as arguments. Why? Because we need to be able to\n// toString() it into a script tag to execute as early as possible in the browser\n// during SSR. Additionally, we also call it from within the router lifecycle\nexport function restoreScroll({\n storageKey,\n key,\n behavior,\n shouldScrollRestoration,\n scrollToTopSelectors,\n location,\n}: {\n storageKey: string\n key?: string\n behavior?: ScrollToOptions['behavior']\n shouldScrollRestoration?: boolean\n scrollToTopSelectors?: Array<string | (() => Element | null | undefined)>\n location?: HistoryLocation\n}) {\n let byKey: ScrollRestorationByKey\n\n try {\n byKey = JSON.parse(sessionStorage.getItem(storageKey) || '{}')\n } catch (error) {\n console.error(error)\n return\n }\n\n const resolvedKey = key || window.history.state?.__TSR_key\n const elementEntries = byKey[resolvedKey]\n\n //\n ignoreScroll = true\n\n //\n scroll: {\n // If we have a cached entry for this location state,\n // we always need to prefer that over the hash scroll.\n if (\n shouldScrollRestoration &&\n elementEntries &&\n Object.keys(elementEntries).length > 0\n ) {\n for (const elementSelector in elementEntries) {\n const entry = elementEntries[elementSelector]!\n if (elementSelector === 'window') {\n window.scrollTo({\n top: entry.scrollY,\n left: entry.scrollX,\n behavior,\n })\n } else if (elementSelector) {\n const element = document.querySelector(elementSelector)\n if (element) {\n element.scrollLeft = entry.scrollX\n element.scrollTop = entry.scrollY\n }\n }\n }\n\n break scroll\n }\n\n // If we don't have a cached entry for the hash,\n // Which means we've never seen this location before,\n // we need to check if there is a hash in the URL.\n // If there is, we need to scroll it's ID into view.\n const hash = (location ?? window.location).hash.split('#', 2)[1]\n\n if (hash) {\n const hashScrollIntoViewOptions =\n window.history.state?.__hashScrollIntoViewOptions ?? true\n\n if (hashScrollIntoViewOptions) {\n const el = document.getElementById(hash)\n if (el) {\n el.scrollIntoView(hashScrollIntoViewOptions)\n }\n }\n\n break scroll\n }\n\n // If there is no cached entry for the hash and there is no hash in the URL,\n // we need to scroll to the top of the page for every scrollToTop element\n const scrollOptions = { top: 0, left: 0, behavior }\n window.scrollTo(scrollOptions)\n if (scrollToTopSelectors) {\n for (const selector of scrollToTopSelectors) {\n if (selector === 'window') continue\n const element =\n typeof selector === 'function'\n ? selector()\n : document.querySelector(selector)\n if (element) element.scrollTo(scrollOptions)\n }\n }\n }\n\n //\n ignoreScroll = false\n}\n\n/** Setup global listeners and hooks to support scroll restoration. */\n/** Setup global listeners and hooks to support scroll restoration. */\nexport function setupScrollRestoration(router: AnyRouter, force?: boolean) {\n if (!scrollRestorationCache && !(isServer ?? router.isServer)) {\n return\n }\n const shouldScrollRestoration =\n force ?? router.options.scrollRestoration ?? false\n\n if (shouldScrollRestoration) {\n router.isScrollRestoring = true\n }\n\n if (\n (isServer ?? router.isServer) ||\n router.isScrollRestorationSetup ||\n !scrollRestorationCache\n ) {\n return\n }\n\n router.isScrollRestorationSetup = true\n\n //\n ignoreScroll = false\n\n const getKey =\n router.options.getScrollRestorationKey || defaultGetScrollRestorationKey\n\n window.history.scrollRestoration = 'manual'\n\n // // Create a MutationObserver to monitor DOM changes\n // const mutationObserver = new MutationObserver(() => {\n // ;ignoreScroll = true\n // requestAnimationFrame(() => {\n // ;ignoreScroll = false\n\n // // Attempt to restore scroll position on each dom\n // // mutation until the user scrolls. We do this\n // // because dynamic content may come in at different\n // // ticks after the initial render and we want to\n // // keep up with that content as much as possible.\n // // As soon as the user scrolls, we no longer need\n // // to attempt router.\n // // console.log('mutation observer restoreScroll')\n // restoreScroll(\n // storageKey,\n // getKey(router.state.location),\n // router.options.scrollRestorationBehavior,\n // )\n // })\n // })\n\n // const observeDom = () => {\n // // Observe changes to the entire document\n // mutationObserver.observe(document, {\n // childList: true, // Detect added or removed child nodes\n // subtree: true, // Monitor all descendants\n // characterData: true, // Detect text content changes\n // })\n // }\n\n // const unobserveDom = () => {\n // mutationObserver.disconnect()\n // }\n\n // observeDom()\n\n const onScroll = (event: Event) => {\n // unobserveDom()\n\n if (ignoreScroll || !router.isScrollRestoring) {\n return\n }\n\n let elementSelector = ''\n\n if (event.target === document || event.target === window) {\n elementSelector = 'window'\n } else {\n const attrId = (event.target as Element).getAttribute(\n 'data-scroll-restoration-id',\n )\n\n if (attrId) {\n elementSelector = `[data-scroll-restoration-id=\"${attrId}\"]`\n } else {\n elementSelector = getCssSelector(event.target)\n }\n }\n\n const restoreKey = getKey(router.state.location)\n\n scrollRestorationCache.set((state) => {\n const keyEntry = (state[restoreKey] ||= {} as ScrollRestorationByElement)\n\n const elementEntry = (keyEntry[elementSelector] ||=\n {} as ScrollRestorationEntry)\n\n if (elementSelector === 'window') {\n elementEntry.scrollX = window.scrollX || 0\n elementEntry.scrollY = window.scrollY || 0\n } else if (elementSelector) {\n const element = document.querySelector(elementSelector)\n if (element) {\n elementEntry.scrollX = element.scrollLeft || 0\n elementEntry.scrollY = element.scrollTop || 0\n }\n }\n\n return state\n })\n }\n\n // Throttle the scroll event to avoid excessive updates\n if (typeof document !== 'undefined') {\n document.addEventListener('scroll', throttle(onScroll, 100), true)\n }\n\n router.subscribe('onRendered', (event) => {\n // unobserveDom()\n\n const cacheKey = getKey(event.toLocation)\n\n // If the user doesn't want to restore the scroll position,\n // we don't need to do anything.\n if (!router.resetNextScroll) {\n router.resetNextScroll = true\n return\n }\n if (typeof router.options.scrollRestoration === 'function') {\n const shouldRestore = router.options.scrollRestoration({\n location: router.latestLocation,\n })\n if (!shouldRestore) {\n return\n }\n }\n\n restoreScroll({\n storageKey,\n key: cacheKey,\n behavior: router.options.scrollRestorationBehavior,\n shouldScrollRestoration: router.isScrollRestoring,\n scrollToTopSelectors: router.options.scrollToTopSelectors,\n location: router.history.location,\n })\n\n if (router.isScrollRestoring) {\n // Mark the location as having been seen\n scrollRestorationCache.set((state) => {\n state[cacheKey] ||= {} as ScrollRestorationByElement\n\n return state\n })\n }\n })\n}\n\n/**\n * @private\n * Handles hash-based scrolling after navigation completes.\n * To be used in framework-specific <Transitioner> components during the onResolved event.\n *\n * Provides hash scrolling for programmatic navigation when default browser handling is prevented.\n * @param router The router instance containing current location and state\n */\n/**\n * @private\n * Handles hash-based scrolling after navigation completes.\n * To be used in framework-specific Transitioners.\n */\nexport function handleHashScroll(router: AnyRouter) {\n if (typeof document !== 'undefined' && (document as any).querySelector) {\n const hashScrollIntoViewOptions =\n router.state.location.state.__hashScrollIntoViewOptions ?? true\n\n if (hashScrollIntoViewOptions && router.state.location.hash !== '') {\n const el = document.getElementById(router.state.location.hash)\n if (el) {\n el.scrollIntoView(hashScrollIntoViewOptions)\n }\n }\n }\n}\n"],"names":["storageKey"],"mappings":";;AAsBA,SAAS,wBAAwB;AAC/B,MAAI;AACF,QACE,OAAO,WAAW,eAClB,OAAO,OAAO,mBAAmB,UACjC;AACA,aAAO,OAAO;AAAA,IAChB;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAKO,MAAM,aAAa;AAE1B,MAAM,WAAW,CAAC,IAAmC,SAAiB;AACpE,MAAI;AACJ,SAAO,IAAI,SAAqB;AAC9B,QAAI,CAAC,SAAS;AACZ,gBAAU,WAAW,MAAM;AACzB,WAAG,GAAG,IAAI;AACV,kBAAU;AAAA,MACZ,GAAG,IAAI;AAAA,IACT;AAAA,EACF;AACF;AAEA,SAAS,+BAA8D;AACrE,QAAM,qBAAqB,sBAAA;AAC3B,MAAI,CAAC,oBAAoB;AACvB,WAAO;AAAA,EACT;AAEA,QAAM,iBAAiB,mBAAmB,QAAQ,UAAU;AAC5D,MAAI,QAAgC,iBAChC,KAAK,MAAM,cAAc,IACzB,CAAA;AAEJ,SAAO;AAAA,IACL;AAAA;AAAA;AAAA;AAAA,IAIA,KAAK,CAAC,YAAY;AAChB,cAAQ,iBAAiB,SAAS,KAAK,KAAK;AAC5C,UAAI;AACF,2BAAmB,QAAQ,YAAY,KAAK,UAAU,KAAK,CAAC;AAAA,MAC9D,QAAQ;AACN,gBAAQ;AAAA,UACN;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF;AAAA,EAAA;AAEJ;AAGO,MAAM,yBAAyB,6BAAA;AAY/B,MAAM,iCAAiC,CAAC,aAA6B;AAC1E,SAAO,SAAS,MAAM,aAAc,SAAS;AAC/C;AAGO,SAAS,eAAe,IAAiB;AAC9C,QAAM,OAAO,CAAA;AACb,MAAI;AACJ,SAAQ,SAAS,GAAG,YAAa;AAC/B,SAAK;AAAA,MACH,GAAG,GAAG,OAAO,cAAc,MAAM,UAAU,QAAQ,KAAK,OAAO,UAAU,EAAE,IAAI,CAAC;AAAA,IAAA;AAElF,SAAK;AAAA,EACP;AACA,SAAO,GAAG,KAAK,QAAA,EAAU,KAAK,KAAK,CAAC,GAAG,YAAA;AACzC;AAEA,IAAI,eAAe;AAMZ,SAAS,cAAc;AAAA,EAC5B,YAAAA;AAAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,MAAI;AAEJ,MAAI;AACF,YAAQ,KAAK,MAAM,eAAe,QAAQA,WAAU,KAAK,IAAI;AAAA,EAC/D,SAAS,OAAO;AACd,YAAQ,MAAM,KAAK;AACnB;AAAA,EACF;AAEA,QAAM,cAAc,OAAO,OAAO,QAAQ,OAAO;AACjD,QAAM,iBAAiB,MAAM,WAAW;AAGxC,iBAAe;AAGf,UAAQ;AAGN,QACE,2BACA,kBACA,OAAO,KAAK,cAAc,EAAE,SAAS,GACrC;AACA,iBAAW,mBAAmB,gBAAgB;AAC5C,cAAM,QAAQ,eAAe,eAAe;AAC5C,YAAI,oBAAoB,UAAU;AAChC,iBAAO,SAAS;AAAA,YACd,KAAK,MAAM;AAAA,YACX,MAAM,MAAM;AAAA,YACZ;AAAA,UAAA,CACD;AAAA,QACH,WAAW,iBAAiB;AAC1B,gBAAM,UAAU,SAAS,cAAc,eAAe;AACtD,cAAI,SAAS;AACX,oBAAQ,aAAa,MAAM;AAC3B,oBAAQ,YAAY,MAAM;AAAA,UAC5B;AAAA,QACF;AAAA,MACF;AAEA,YAAM;AAAA,IACR;AAMA,UAAM,QAAQ,YAAY,OAAO,UAAU,KAAK,MAAM,KAAK,CAAC,EAAE,CAAC;AAE/D,QAAI,MAAM;AACR,YAAM,4BACJ,OAAO,QAAQ,OAAO,+BAA+B;AAEvD,UAAI,2BAA2B;AAC7B,cAAM,KAAK,SAAS,eAAe,IAAI;AACvC,YAAI,IAAI;AACN,aAAG,eAAe,yBAAyB;AAAA,QAC7C;AAAA,MACF;AAEA,YAAM;AAAA,IACR;AAIA,UAAM,gBAAgB,EAAE,KAAK,GAAG,MAAM,GAAG,SAAA;AACzC,WAAO,SAAS,aAAa;AAC7B,QAAI,sBAAsB;AACxB,iBAAW,YAAY,sBAAsB;AAC3C,YAAI,aAAa,SAAU;AAC3B,cAAM,UACJ,OAAO,aAAa,aAChB,aACA,SAAS,cAAc,QAAQ;AACrC,YAAI,QAAS,SAAQ,SAAS,aAAa;AAAA,MAC7C;AAAA,IACF;AAAA,EACF;AAGA,iBAAe;AACjB;AAIO,SAAS,uBAAuB,QAAmB,OAAiB;AACzE,MAAI,CAAC,0BAA0B,EAAE,YAAY,OAAO,WAAW;AAC7D;AAAA,EACF;AACA,QAAM,0BACJ,SAAS,OAAO,QAAQ,qBAAqB;AAE/C,MAAI,yBAAyB;AAC3B,WAAO,oBAAoB;AAAA,EAC7B;AAEA,OACG,YAAY,OAAO,aACpB,OAAO,4BACP,CAAC,wBACD;AACA;AAAA,EACF;AAEA,SAAO,2BAA2B;AAGlC,iBAAe;AAEf,QAAM,SACJ,OAAO,QAAQ,2BAA2B;AAE5C,SAAO,QAAQ,oBAAoB;AAuCnC,QAAM,WAAW,CAAC,UAAiB;AAGjC,QAAI,gBAAgB,CAAC,OAAO,mBAAmB;AAC7C;AAAA,IACF;AAEA,QAAI,kBAAkB;AAEtB,QAAI,MAAM,WAAW,YAAY,MAAM,WAAW,QAAQ;AACxD,wBAAkB;AAAA,IACpB,OAAO;AACL,YAAM,SAAU,MAAM,OAAmB;AAAA,QACvC;AAAA,MAAA;AAGF,UAAI,QAAQ;AACV,0BAAkB,gCAAgC,MAAM;AAAA,MAC1D,OAAO;AACL,0BAAkB,eAAe,MAAM,MAAM;AAAA,MAC/C;AAAA,IACF;AAEA,UAAM,aAAa,OAAO,OAAO,MAAM,QAAQ;AAE/C,2BAAuB,IAAI,CAAC,UAAU;AACpC,YAAM,WAAY,MAAM,UAAU,MAAM,CAAA;AAExC,YAAM,eAAgB,SAAS,eAAe,MAC5C,CAAA;AAEF,UAAI,oBAAoB,UAAU;AAChC,qBAAa,UAAU,OAAO,WAAW;AACzC,qBAAa,UAAU,OAAO,WAAW;AAAA,MAC3C,WAAW,iBAAiB;AAC1B,cAAM,UAAU,SAAS,cAAc,eAAe;AACtD,YAAI,SAAS;AACX,uBAAa,UAAU,QAAQ,cAAc;AAC7C,uBAAa,UAAU,QAAQ,aAAa;AAAA,QAC9C;AAAA,MACF;AAEA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAGA,MAAI,OAAO,aAAa,aAAa;AACnC,aAAS,iBAAiB,UAAU,SAAS,UAAU,GAAG,GAAG,IAAI;AAAA,EACnE;AAEA,SAAO,UAAU,cAAc,CAAC,UAAU;AAGxC,UAAM,WAAW,OAAO,MAAM,UAAU;AAIxC,QAAI,CAAC,OAAO,iBAAiB;AAC3B,aAAO,kBAAkB;AACzB;AAAA,IACF;AACA,QAAI,OAAO,OAAO,QAAQ,sBAAsB,YAAY;AAC1D,YAAM,gBAAgB,OAAO,QAAQ,kBAAkB;AAAA,QACrD,UAAU,OAAO;AAAA,MAAA,CAClB;AACD,UAAI,CAAC,eAAe;AAClB;AAAA,MACF;AAAA,IACF;AAEA,kBAAc;AAAA,MACZ;AAAA,MACA,KAAK;AAAA,MACL,UAAU,OAAO,QAAQ;AAAA,MACzB,yBAAyB,OAAO;AAAA,MAChC,sBAAsB,OAAO,QAAQ;AAAA,MACrC,UAAU,OAAO,QAAQ;AAAA,IAAA,CAC1B;AAED,QAAI,OAAO,mBAAmB;AAE5B,6BAAuB,IAAI,CAAC,UAAU;AACpC,cAAM,QAAQ,MAAM,CAAA;AAEpB,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAeO,SAAS,iBAAiB,QAAmB;AAClD,MAAI,OAAO,aAAa,eAAgB,SAAiB,eAAe;AACtE,UAAM,4BACJ,OAAO,MAAM,SAAS,MAAM,+BAA+B;AAE7D,QAAI,6BAA6B,OAAO,MAAM,SAAS,SAAS,IAAI;AAClE,YAAM,KAAK,SAAS,eAAe,OAAO,MAAM,SAAS,IAAI;AAC7D,UAAI,IAAI;AACN,WAAG,eAAe,yBAAyB;AAAA,MAC7C;AAAA,IACF;AAAA,EACF;AACF;"}
|
|
@@ -162,7 +162,7 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
162
162
|
if (cleanedUp || isStreamClosed) return;
|
|
163
163
|
const html = router.serverSsr?.takeBufferedHtml();
|
|
164
164
|
if (!html) return;
|
|
165
|
-
if (isAppRendering) {
|
|
165
|
+
if (isAppRendering || leftover) {
|
|
166
166
|
pendingRouterHtmlParts.push(html);
|
|
167
167
|
} else {
|
|
168
168
|
safeEnqueue(html);
|
|
@@ -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) {\n // Buffer for insertion at next valid position\n pendingRouterHtmlParts.push(html)\n } else {\n // App is done rendering, write directly to output\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 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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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -105,7 +105,6 @@ export {
|
|
|
105
105
|
export { encode, decode } from './qss'
|
|
106
106
|
export { rootRouteId } from './root'
|
|
107
107
|
export type { RootRouteId } from './root'
|
|
108
|
-
export { isServer } from './isServer'
|
|
109
108
|
|
|
110
109
|
export { BaseRoute, BaseRouteApi, BaseRootRoute } from './route'
|
|
111
110
|
export type {
|
package/src/load-matches.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { batch } from '@tanstack/store'
|
|
2
2
|
import invariant from 'tiny-invariant'
|
|
3
|
+
import { isServer } from '@tanstack/router-core/isServer'
|
|
3
4
|
import { createControlledPromise, isPromise } from './utils'
|
|
4
5
|
import { isNotFound } from './not-found'
|
|
5
6
|
import { rootRouteId } from './root'
|
|
6
7
|
import { isRedirect } from './redirect'
|
|
7
|
-
import { isServer } from './isServer'
|
|
8
8
|
import type { NotFoundError } from './not-found'
|
|
9
9
|
import type { ParsedLocation } from './location'
|
|
10
10
|
import type {
|
package/src/router.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Store, batch } from '@tanstack/store'
|
|
2
2
|
import { createBrowserHistory, parseHref } from '@tanstack/history'
|
|
3
|
+
import { isServer } from '@tanstack/router-core/isServer'
|
|
3
4
|
import {
|
|
4
5
|
createControlledPromise,
|
|
5
6
|
decodePath,
|
|
@@ -26,7 +27,6 @@ import {
|
|
|
26
27
|
trimPath,
|
|
27
28
|
trimPathRight,
|
|
28
29
|
} from './path'
|
|
29
|
-
import { isServer } from './isServer'
|
|
30
30
|
import { createLRUCache } from './lru-cache'
|
|
31
31
|
import { isNotFound } from './not-found'
|
|
32
32
|
import { setupScrollRestoration } from './scroll-restoration'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { isServer } from '@tanstack/router-core/isServer'
|
|
1
2
|
import { functionalUpdate } from './utils'
|
|
2
|
-
import { isServer } from './isServer'
|
|
3
3
|
import type { AnyRouter } from './router'
|
|
4
4
|
import type { ParsedLocation } from './location'
|
|
5
5
|
import type { NonNullableUpdater } from './utils'
|
|
@@ -271,11 +271,13 @@ export function transformStreamWithRouter(
|
|
|
271
271
|
const html = router.serverSsr?.takeBufferedHtml()
|
|
272
272
|
if (!html) return
|
|
273
273
|
|
|
274
|
-
if (isAppRendering) {
|
|
275
|
-
// Buffer
|
|
274
|
+
if (isAppRendering || leftover) {
|
|
275
|
+
// Buffer when app is still rendering OR when there's leftover content
|
|
276
|
+
// that hasn't been flushed yet. This prevents race conditions where
|
|
277
|
+
// injected HTML appears before buffered app content
|
|
276
278
|
pendingRouterHtmlParts.push(html)
|
|
277
279
|
} else {
|
|
278
|
-
// App
|
|
280
|
+
// App done rendering and no leftover - safe to write directly for better streaming
|
|
279
281
|
safeEnqueue(html)
|
|
280
282
|
}
|
|
281
283
|
})
|
package/dist/cjs/isServer.d.cts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Static server/client detection for tree-shaking support.
|
|
3
|
-
*
|
|
4
|
-
* This re-exports `isServer` from the `isServer/` directory which uses
|
|
5
|
-
* conditional exports to provide different values based on the environment:
|
|
6
|
-
*
|
|
7
|
-
* - `browser` condition → `false` (client)
|
|
8
|
-
* - `node`/`worker`/`deno`/`bun` → `true` (server)
|
|
9
|
-
* - `development` condition → `undefined` (for tests, falls back to router.isServer)
|
|
10
|
-
*
|
|
11
|
-
* The bundler resolves the correct file at build time based on export conditions,
|
|
12
|
-
* and since the value is a literal constant, dead code can be eliminated.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```typescript
|
|
16
|
-
* import { isServer } from '@tanstack/router-core'
|
|
17
|
-
*
|
|
18
|
-
* // The ?? operator provides fallback for development/test mode
|
|
19
|
-
* if (isServer ?? router.isServer) {
|
|
20
|
-
* // Server-only code - eliminated in client bundles
|
|
21
|
-
* }
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
export { isServer } from './isServer/development.cjs';
|
package/dist/esm/isServer.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Static server/client detection for tree-shaking support.
|
|
3
|
-
*
|
|
4
|
-
* This re-exports `isServer` from the `isServer/` directory which uses
|
|
5
|
-
* conditional exports to provide different values based on the environment:
|
|
6
|
-
*
|
|
7
|
-
* - `browser` condition → `false` (client)
|
|
8
|
-
* - `node`/`worker`/`deno`/`bun` → `true` (server)
|
|
9
|
-
* - `development` condition → `undefined` (for tests, falls back to router.isServer)
|
|
10
|
-
*
|
|
11
|
-
* The bundler resolves the correct file at build time based on export conditions,
|
|
12
|
-
* and since the value is a literal constant, dead code can be eliminated.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```typescript
|
|
16
|
-
* import { isServer } from '@tanstack/router-core'
|
|
17
|
-
*
|
|
18
|
-
* // The ?? operator provides fallback for development/test mode
|
|
19
|
-
* if (isServer ?? router.isServer) {
|
|
20
|
-
* // Server-only code - eliminated in client bundles
|
|
21
|
-
* }
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
export { isServer } from './isServer/development.js';
|
package/src/isServer.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Static server/client detection for tree-shaking support.
|
|
3
|
-
*
|
|
4
|
-
* This re-exports `isServer` from the `isServer/` directory which uses
|
|
5
|
-
* conditional exports to provide different values based on the environment:
|
|
6
|
-
*
|
|
7
|
-
* - `browser` condition → `false` (client)
|
|
8
|
-
* - `node`/`worker`/`deno`/`bun` → `true` (server)
|
|
9
|
-
* - `development` condition → `undefined` (for tests, falls back to router.isServer)
|
|
10
|
-
*
|
|
11
|
-
* The bundler resolves the correct file at build time based on export conditions,
|
|
12
|
-
* and since the value is a literal constant, dead code can be eliminated.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```typescript
|
|
16
|
-
* import { isServer } from '@tanstack/router-core'
|
|
17
|
-
*
|
|
18
|
-
* // The ?? operator provides fallback for development/test mode
|
|
19
|
-
* if (isServer ?? router.isServer) {
|
|
20
|
-
* // Server-only code - eliminated in client bundles
|
|
21
|
-
* }
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
export { isServer } from './isServer/development'
|