@tanstack/router-core 1.127.0 → 1.127.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  import invariant from "tiny-invariant";
2
+ import { batch } from "@tanstack/store";
3
+ import { createControlledPromise } from "../utils.js";
2
4
  function hydrateMatch(deyhydratedMatch) {
3
5
  return {
4
6
  id: deyhydratedMatch.i,
@@ -27,6 +29,23 @@ async function hydrate(router) {
27
29
  return router.loadRouteChunk(route);
28
30
  })
29
31
  );
32
+ function setMatchForcePending(match) {
33
+ const route = router.looseRoutesById[match.routeId];
34
+ const pendingMinMs = route.options.pendingMinMs ?? router.options.defaultPendingMinMs;
35
+ if (pendingMinMs) {
36
+ const minPendingPromise = createControlledPromise();
37
+ match.minPendingPromise = minPendingPromise;
38
+ match._forcePending = true;
39
+ setTimeout(() => {
40
+ minPendingPromise.resolve();
41
+ router.updateMatch(match.id, (prev) => ({
42
+ ...prev,
43
+ minPendingPromise: void 0,
44
+ _forcePending: void 0
45
+ }));
46
+ }, pendingMinMs);
47
+ }
48
+ }
30
49
  let firstNonSsrMatchIndex = void 0;
31
50
  matches.forEach((match) => {
32
51
  const dehydratedMatch = window.$_TSR.router.matches.find(
@@ -45,12 +64,9 @@ async function hydrate(router) {
45
64
  if (match.ssr === "data-only" || match.ssr === false) {
46
65
  if (firstNonSsrMatchIndex === void 0) {
47
66
  firstNonSsrMatchIndex = match.index;
48
- match._forcePending = true;
67
+ setMatchForcePending(match);
49
68
  }
50
69
  }
51
- if (match.ssr === false) {
52
- return;
53
- }
54
70
  });
55
71
  router.__store.setState((s) => {
56
72
  return {
@@ -98,27 +114,42 @@ async function hydrate(router) {
98
114
  match.scripts = scripts;
99
115
  })
100
116
  );
117
+ const isSpaMode = matches[matches.length - 1].id !== lastMatchId;
118
+ const hasSsrFalseMatches = matches.some((m) => m.ssr === false);
119
+ if (!hasSsrFalseMatches && !isSpaMode) {
120
+ matches.forEach((match) => {
121
+ match._dehydrated = void 0;
122
+ });
123
+ return routeChunkPromise;
124
+ }
101
125
  const loadPromise = Promise.resolve().then(() => router.load()).catch((err) => {
102
126
  console.error("Error during router hydration:", err);
103
127
  });
104
- if (matches[matches.length - 1].id !== lastMatchId) {
105
- const matchId = matches[0].id;
106
- router.updateMatch(matchId, (prev) => {
107
- return {
108
- ...prev,
109
- _displayPending: true,
110
- displayPendingPromise: loadPromise,
111
- // make sure that the pending component is displayed for at least pendingMinMs
112
- _forcePending: true
113
- };
114
- });
128
+ if (isSpaMode) {
129
+ const match = matches[1];
130
+ invariant(
131
+ match,
132
+ "Expected to find a match below the root match in SPA mode."
133
+ );
134
+ setMatchForcePending(match);
135
+ match._displayPending = true;
136
+ match.displayPendingPromise = loadPromise;
115
137
  loadPromise.then(() => {
116
- router.updateMatch(matchId, (prev) => {
117
- return {
118
- ...prev,
119
- _displayPending: void 0,
120
- displayPendingPromise: void 0
121
- };
138
+ batch(() => {
139
+ if (router.__store.state.status === "pending") {
140
+ router.__store.setState((s) => ({
141
+ ...s,
142
+ status: "idle",
143
+ resolvedLocation: s.location
144
+ }));
145
+ }
146
+ router.updateMatch(match.id, (prev) => {
147
+ return {
148
+ ...prev,
149
+ _displayPending: void 0,
150
+ displayPendingPromise: void 0
151
+ };
152
+ });
122
153
  });
123
154
  });
124
155
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-client.js","sources":["../../../src/ssr/ssr-client.ts"],"sourcesContent":["import invariant from 'tiny-invariant'\nimport type { MakeRouteMatch } from '../Matches'\nimport type { AnyRouter } from '../router'\nimport type { Manifest } from '../manifest'\nimport type { RouteContextOptions } from '../route'\nimport type { GLOBAL_TSR } from './ssr-server'\n\ndeclare global {\n interface Window {\n [GLOBAL_TSR]?: TsrSsrGlobal\n }\n}\n\nexport interface TsrSsrGlobal {\n router?: DehydratedRouter\n // clean scripts, shortened since this is sent for each streamed script\n c: () => void\n}\n\nfunction hydrateMatch(\n deyhydratedMatch: DehydratedMatch,\n): Partial<MakeRouteMatch> {\n return {\n id: deyhydratedMatch.i,\n __beforeLoadContext: deyhydratedMatch.b,\n loaderData: deyhydratedMatch.l,\n status: deyhydratedMatch.s,\n ssr: deyhydratedMatch.ssr,\n updatedAt: deyhydratedMatch.u,\n error: deyhydratedMatch.e,\n }\n}\nexport interface DehydratedMatch {\n i: MakeRouteMatch['id']\n b?: MakeRouteMatch['__beforeLoadContext']\n l?: MakeRouteMatch['loaderData']\n e?: MakeRouteMatch['error']\n u: MakeRouteMatch['updatedAt']\n s: MakeRouteMatch['status']\n ssr?: MakeRouteMatch['ssr']\n}\n\nexport interface DehydratedRouter {\n manifest: Manifest | undefined\n dehydratedData?: any\n lastMatchId?: string\n matches: Array<DehydratedMatch>\n}\n\nexport async function hydrate(router: AnyRouter): Promise<any> {\n invariant(\n window.$_TSR?.router,\n 'Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',\n )\n\n const { manifest, dehydratedData, lastMatchId } = window.$_TSR.router\n\n router.ssr = {\n manifest,\n }\n\n // Hydrate the router state\n const matches = router.matchRoutes(router.state.location)\n\n // kick off loading the route chunks\n const routeChunkPromise = Promise.all(\n matches.map((match) => {\n const route = router.looseRoutesById[match.routeId]!\n return router.loadRouteChunk(route)\n }),\n )\n\n // Right after hydration and before the first render, we need to rehydrate each match\n // First step is to reyhdrate loaderData and __beforeLoadContext\n let firstNonSsrMatchIndex: number | undefined = undefined\n matches.forEach((match) => {\n const dehydratedMatch = window.$_TSR!.router!.matches.find(\n (d) => d.i === match.id,\n )\n if (!dehydratedMatch) {\n Object.assign(match, { dehydrated: false, ssr: false })\n return\n }\n\n Object.assign(match, hydrateMatch(dehydratedMatch))\n\n if (match.ssr === false) {\n match._dehydrated = false\n } else {\n match._dehydrated = true\n }\n\n if (match.ssr === 'data-only' || match.ssr === false) {\n if (firstNonSsrMatchIndex === undefined) {\n firstNonSsrMatchIndex = match.index\n match._forcePending = true\n }\n }\n\n if (match.ssr === false) {\n return\n }\n })\n\n router.__store.setState((s) => {\n return {\n ...s,\n matches,\n }\n })\n\n // Allow the user to handle custom hydration data\n await router.options.hydrate?.(dehydratedData)\n\n // now that all necessary data is hydrated:\n // 1) fully reconstruct the route context\n // 2) execute `head()` and `scripts()` for each match\n await Promise.all(\n router.state.matches.map(async (match) => {\n const route = router.looseRoutesById[match.routeId]!\n\n const parentMatch = router.state.matches[match.index - 1]\n const parentContext = parentMatch?.context ?? router.options.context ?? {}\n\n // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed\n // so run it again and merge route context\n const contextFnContext: RouteContextOptions<any, any, any, any> = {\n deps: match.loaderDeps,\n params: match.params,\n context: parentContext,\n location: router.state.location,\n navigate: (opts: any) =>\n router.navigate({ ...opts, _fromLocation: router.state.location }),\n buildLocation: router.buildLocation,\n cause: match.cause,\n abortController: match.abortController,\n preload: false,\n matches,\n }\n match.__routeContext = route.options.context?.(contextFnContext) ?? {}\n\n match.context = {\n ...parentContext,\n ...match.__routeContext,\n ...match.__beforeLoadContext,\n }\n\n const assetContext = {\n matches: router.state.matches,\n match,\n params: match.params,\n loaderData: match.loaderData,\n }\n const headFnContent = await route.options.head?.(assetContext)\n\n const scripts = await route.options.scripts?.(assetContext)\n\n match.meta = headFnContent?.meta\n match.links = headFnContent?.links\n match.headScripts = headFnContent?.scripts\n match.styles = headFnContent?.styles\n match.scripts = scripts\n }),\n )\n\n // schedule router.load() to run after the next tick so we can store the promise in the match before loading starts\n const loadPromise = Promise.resolve()\n .then(() => router.load())\n .catch((err) => {\n console.error('Error during router hydration:', err)\n })\n\n // in SPA mode we need to keep the outermost match pending until router.load() is finished\n // this will prevent that other pending components are rendered but hydration is not blocked\n if (matches[matches.length - 1]!.id !== lastMatchId) {\n const matchId = matches[0]!.id\n router.updateMatch(matchId, (prev) => {\n return {\n ...prev,\n _displayPending: true,\n displayPendingPromise: loadPromise,\n // make sure that the pending component is displayed for at least pendingMinMs\n _forcePending: true,\n }\n })\n // hide the pending component once the load is finished\n loadPromise.then(() => {\n router.updateMatch(matchId, (prev) => {\n return {\n ...prev,\n _displayPending: undefined,\n displayPendingPromise: undefined,\n }\n })\n })\n }\n return routeChunkPromise\n}\n"],"names":["_b","_a","_c"],"mappings":"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"}
1
+ {"version":3,"file":"ssr-client.js","sources":["../../../src/ssr/ssr-client.ts"],"sourcesContent":["import invariant from 'tiny-invariant'\nimport { batch } from '@tanstack/store'\nimport { createControlledPromise } from '../utils'\nimport type { AnyRouteMatch, MakeRouteMatch } from '../Matches'\nimport type { AnyRouter } from '../router'\nimport type { Manifest } from '../manifest'\nimport type { RouteContextOptions } from '../route'\nimport type { GLOBAL_TSR } from './ssr-server'\n\ndeclare global {\n interface Window {\n [GLOBAL_TSR]?: TsrSsrGlobal\n }\n}\n\nexport interface TsrSsrGlobal {\n router?: DehydratedRouter\n // clean scripts, shortened since this is sent for each streamed script\n c: () => void\n}\n\nfunction hydrateMatch(\n deyhydratedMatch: DehydratedMatch,\n): Partial<MakeRouteMatch> {\n return {\n id: deyhydratedMatch.i,\n __beforeLoadContext: deyhydratedMatch.b,\n loaderData: deyhydratedMatch.l,\n status: deyhydratedMatch.s,\n ssr: deyhydratedMatch.ssr,\n updatedAt: deyhydratedMatch.u,\n error: deyhydratedMatch.e,\n }\n}\nexport interface DehydratedMatch {\n i: MakeRouteMatch['id']\n b?: MakeRouteMatch['__beforeLoadContext']\n l?: MakeRouteMatch['loaderData']\n e?: MakeRouteMatch['error']\n u: MakeRouteMatch['updatedAt']\n s: MakeRouteMatch['status']\n ssr?: MakeRouteMatch['ssr']\n}\n\nexport interface DehydratedRouter {\n manifest: Manifest | undefined\n dehydratedData?: any\n lastMatchId?: string\n matches: Array<DehydratedMatch>\n}\n\nexport async function hydrate(router: AnyRouter): Promise<any> {\n invariant(\n window.$_TSR?.router,\n 'Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',\n )\n\n const { manifest, dehydratedData, lastMatchId } = window.$_TSR.router\n\n router.ssr = {\n manifest,\n }\n\n // Hydrate the router state\n const matches = router.matchRoutes(router.state.location)\n\n // kick off loading the route chunks\n const routeChunkPromise = Promise.all(\n matches.map((match) => {\n const route = router.looseRoutesById[match.routeId]!\n return router.loadRouteChunk(route)\n }),\n )\n\n function setMatchForcePending(match: AnyRouteMatch) {\n // usually the minPendingPromise is created in the Match component if a pending match is rendered\n // however, this might be too late if the match synchronously resolves\n const route = router.looseRoutesById[match.routeId]!\n const pendingMinMs =\n route.options.pendingMinMs ?? router.options.defaultPendingMinMs\n if (pendingMinMs) {\n const minPendingPromise = createControlledPromise<void>()\n match.minPendingPromise = minPendingPromise\n match._forcePending = true\n\n setTimeout(() => {\n minPendingPromise.resolve()\n // We've handled the minPendingPromise, so we can delete it\n router.updateMatch(match.id, (prev) => ({\n ...prev,\n minPendingPromise: undefined,\n _forcePending: undefined,\n }))\n }, pendingMinMs)\n }\n }\n\n // Right after hydration and before the first render, we need to rehydrate each match\n // First step is to reyhdrate loaderData and __beforeLoadContext\n let firstNonSsrMatchIndex: number | undefined = undefined\n matches.forEach((match) => {\n const dehydratedMatch = window.$_TSR!.router!.matches.find(\n (d) => d.i === match.id,\n )\n if (!dehydratedMatch) {\n Object.assign(match, { dehydrated: false, ssr: false })\n return\n }\n\n Object.assign(match, hydrateMatch(dehydratedMatch))\n\n if (match.ssr === false) {\n match._dehydrated = false\n } else {\n match._dehydrated = true\n }\n\n if (match.ssr === 'data-only' || match.ssr === false) {\n if (firstNonSsrMatchIndex === undefined) {\n firstNonSsrMatchIndex = match.index\n setMatchForcePending(match)\n }\n }\n })\n\n router.__store.setState((s) => {\n return {\n ...s,\n matches,\n }\n })\n\n // Allow the user to handle custom hydration data\n await router.options.hydrate?.(dehydratedData)\n\n // now that all necessary data is hydrated:\n // 1) fully reconstruct the route context\n // 2) execute `head()` and `scripts()` for each match\n await Promise.all(\n router.state.matches.map(async (match) => {\n const route = router.looseRoutesById[match.routeId]!\n\n const parentMatch = router.state.matches[match.index - 1]\n const parentContext = parentMatch?.context ?? router.options.context ?? {}\n\n // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed\n // so run it again and merge route context\n const contextFnContext: RouteContextOptions<any, any, any, any> = {\n deps: match.loaderDeps,\n params: match.params,\n context: parentContext,\n location: router.state.location,\n navigate: (opts: any) =>\n router.navigate({ ...opts, _fromLocation: router.state.location }),\n buildLocation: router.buildLocation,\n cause: match.cause,\n abortController: match.abortController,\n preload: false,\n matches,\n }\n match.__routeContext = route.options.context?.(contextFnContext) ?? {}\n\n match.context = {\n ...parentContext,\n ...match.__routeContext,\n ...match.__beforeLoadContext,\n }\n\n const assetContext = {\n matches: router.state.matches,\n match,\n params: match.params,\n loaderData: match.loaderData,\n }\n const headFnContent = await route.options.head?.(assetContext)\n\n const scripts = await route.options.scripts?.(assetContext)\n\n match.meta = headFnContent?.meta\n match.links = headFnContent?.links\n match.headScripts = headFnContent?.scripts\n match.styles = headFnContent?.styles\n match.scripts = scripts\n }),\n )\n\n const isSpaMode = matches[matches.length - 1]!.id !== lastMatchId\n const hasSsrFalseMatches = matches.some((m) => m.ssr === false)\n // all matches have data from the server and we are not in SPA mode so we don't need to kick of router.load()\n if (!hasSsrFalseMatches && !isSpaMode) {\n matches.forEach((match) => {\n // remove the _dehydrate flag since we won't run router.load() which would remove it\n match._dehydrated = undefined\n })\n return routeChunkPromise\n }\n\n // schedule router.load() to run after the next tick so we can store the promise in the match before loading starts\n const loadPromise = Promise.resolve()\n .then(() => router.load())\n .catch((err) => {\n console.error('Error during router hydration:', err)\n })\n\n // in SPA mode we need to keep the first match below the root route pending until router.load() is finished\n // this will prevent that other pending components are rendered but hydration is not blocked\n if (isSpaMode) {\n const match = matches[1]\n invariant(\n match,\n 'Expected to find a match below the root match in SPA mode.',\n )\n setMatchForcePending(match)\n\n match._displayPending = true\n match.displayPendingPromise = loadPromise\n\n loadPromise.then(() => {\n batch(() => {\n // ensure router is not in status 'pending' anymore\n // this usually happens in Transitioner but if loading synchronously resolves,\n // Transitioner won't be rendered while loading so it cannot track the change from loading:true to loading:false\n if (router.__store.state.status === 'pending') {\n router.__store.setState((s) => ({\n ...s,\n status: 'idle',\n resolvedLocation: s.location,\n }))\n }\n // hide the pending component once the load is finished\n router.updateMatch(match.id, (prev) => {\n return {\n ...prev,\n _displayPending: undefined,\n displayPendingPromise: undefined,\n }\n })\n })\n })\n }\n return routeChunkPromise\n}\n"],"names":["_b","_a","_c"],"mappings":"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"}
@@ -61,12 +61,16 @@ function attachRouterServerSsrUtils(router, manifest) {
61
61
  dehydrate: async () => {
62
62
  var _a, _b, _c;
63
63
  invariant(!_dehydrated, "router is already dehydrated!");
64
- const matches = router.state.matches.map(dehydrateMatch);
64
+ let matchesToDehydrate = router.state.matches;
65
+ if (router.isShell()) {
66
+ matchesToDehydrate = matchesToDehydrate.slice(0, 1);
67
+ }
68
+ const matches = matchesToDehydrate.map(dehydrateMatch);
65
69
  const dehydratedRouter = {
66
70
  manifest: router.ssr.manifest,
67
71
  matches
68
72
  };
69
- const lastMatchId = (_a = router.state.matches[router.state.matches.length - 1]) == null ? void 0 : _a.id;
73
+ const lastMatchId = (_a = matchesToDehydrate[matchesToDehydrate.length - 1]) == null ? void 0 : _a.id;
70
74
  if (lastMatchId) {
71
75
  dehydratedRouter.lastMatchId = lastMatchId;
72
76
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-server.js","sources":["../../../src/ssr/ssr-server.ts"],"sourcesContent":["import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'\nimport { ReadableStreamPlugin } from 'seroval-plugins/web'\nimport invariant from 'tiny-invariant'\nimport { createControlledPromise } from '../utils'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { ShallowErrorPlugin } from './seroval-plugins'\nimport type { AnyRouter } from '../router'\nimport type { DehydratedMatch } from './ssr-client'\nimport type { DehydratedRouter } from './client'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest } from '../manifest'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n promise: Promise<string>\n }\n }\n}\n\nexport const GLOBAL_TSR = '$_TSR'\nconst SCOPE_ID = 'tsr'\n\nexport function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {\n const dehydratedMatch: DehydratedMatch = {\n i: match.id,\n u: match.updatedAt,\n s: match.status,\n }\n\n const properties = [\n ['__beforeLoadContext', 'b'],\n ['loaderData', 'l'],\n ['error', 'e'],\n ['ssr', 'ssr'],\n ] as const\n\n for (const [key, shorthand] of properties) {\n if (match[key] !== undefined) {\n dehydratedMatch[shorthand] = match[key]\n }\n }\n return dehydratedMatch\n}\n\nexport function attachRouterServerSsrUtils(\n router: AnyRouter,\n manifest: Manifest | undefined,\n) {\n router.ssr = {\n manifest,\n }\n const serializationRefs = new Map<unknown, number>()\n\n let initialScriptSent = false\n const getInitialScript = () => {\n if (initialScriptSent) {\n return ''\n }\n initialScriptSent = true\n return `${getCrossReferenceHeader(SCOPE_ID)};${minifiedTsrBootStrapScript};`\n }\n let _dehydrated = false\n const listeners: Array<() => void> = []\n\n router.serverSsr = {\n injectedHtml: [],\n injectHtml: (getHtml) => {\n const promise = Promise.resolve().then(getHtml)\n router.serverSsr!.injectedHtml.push(promise)\n router.emit({\n type: 'onInjectedHtml',\n promise,\n })\n\n return promise.then(() => {})\n },\n injectScript: (getScript) => {\n return router.serverSsr!.injectHtml(async () => {\n const script = await getScript()\n return `<script class='$tsr'>${getInitialScript()}${script};if (typeof $_TSR !== 'undefined') $_TSR.c()</script>`\n })\n },\n dehydrate: async () => {\n invariant(!_dehydrated, 'router is already dehydrated!')\n const matches = router.state.matches.map(dehydrateMatch)\n\n const dehydratedRouter: DehydratedRouter = {\n manifest: router.ssr!.manifest,\n matches,\n }\n const lastMatchId =\n router.state.matches[router.state.matches.length - 1]?.id\n if (lastMatchId) {\n dehydratedRouter.lastMatchId = lastMatchId\n }\n dehydratedRouter.dehydratedData = await router.options.dehydrate?.()\n _dehydrated = true\n\n const p = createControlledPromise<string>()\n crossSerializeStream(dehydratedRouter, {\n refs: serializationRefs,\n // TODO make plugins configurable\n plugins: [ReadableStreamPlugin, ShallowErrorPlugin],\n onSerialize: (data, initial) => {\n const serialized = initial ? `${GLOBAL_TSR}[\"router\"]=` + data : data\n router.serverSsr!.injectScript(() => serialized)\n },\n scopeId: SCOPE_ID,\n onDone: () => p.resolve(''),\n onError: (err) => p.reject(err),\n })\n // make sure the stream is kept open until the promise is resolved\n router.serverSsr!.injectHtml(() => p)\n },\n isDehydrated() {\n return _dehydrated\n },\n onRenderFinished: (listener) => listeners.push(listener),\n setRenderFinished: () => {\n listeners.forEach((l) => l())\n },\n }\n}\n"],"names":[],"mappings":";;;;;;AAwBO,MAAM,aAAa;AAC1B,MAAM,WAAW;AAEV,SAAS,eAAe,OAAuC;AACpE,QAAM,kBAAmC;AAAA,IACvC,GAAG,MAAM;AAAA,IACT,GAAG,MAAM;AAAA,IACT,GAAG,MAAM;AAAA,EACX;AAEA,QAAM,aAAa;AAAA,IACjB,CAAC,uBAAuB,GAAG;AAAA,IAC3B,CAAC,cAAc,GAAG;AAAA,IAClB,CAAC,SAAS,GAAG;AAAA,IACb,CAAC,OAAO,KAAK;AAAA,EACf;AAEA,aAAW,CAAC,KAAK,SAAS,KAAK,YAAY;AACrC,QAAA,MAAM,GAAG,MAAM,QAAW;AACZ,sBAAA,SAAS,IAAI,MAAM,GAAG;AAAA,IAAA;AAAA,EACxC;AAEK,SAAA;AACT;AAEgB,SAAA,2BACd,QACA,UACA;AACA,SAAO,MAAM;AAAA,IACX;AAAA,EACF;AACM,QAAA,wCAAwB,IAAqB;AAEnD,MAAI,oBAAoB;AACxB,QAAM,mBAAmB,MAAM;AAC7B,QAAI,mBAAmB;AACd,aAAA;AAAA,IAAA;AAEW,wBAAA;AACpB,WAAO,GAAG,wBAAwB,QAAQ,CAAC,IAAI,0BAA0B;AAAA,EAC3E;AACA,MAAI,cAAc;AAClB,QAAM,YAA+B,CAAC;AAEtC,SAAO,YAAY;AAAA,IACjB,cAAc,CAAC;AAAA,IACf,YAAY,CAAC,YAAY;AACvB,YAAM,UAAU,QAAQ,QAAQ,EAAE,KAAK,OAAO;AACvC,aAAA,UAAW,aAAa,KAAK,OAAO;AAC3C,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN;AAAA,MAAA,CACD;AAEM,aAAA,QAAQ,KAAK,MAAM;AAAA,MAAA,CAAE;AAAA,IAC9B;AAAA,IACA,cAAc,CAAC,cAAc;AACpB,aAAA,OAAO,UAAW,WAAW,YAAY;AACxC,cAAA,SAAS,MAAM,UAAU;AAC/B,eAAO,wBAAwB,iBAAkB,CAAA,GAAG,MAAM;AAAA,MAAA,CAC3D;AAAA,IACH;AAAA,IACA,WAAW,YAAY;;AACX,gBAAA,CAAC,aAAa,+BAA+B;AACvD,YAAM,UAAU,OAAO,MAAM,QAAQ,IAAI,cAAc;AAEvD,YAAM,mBAAqC;AAAA,QACzC,UAAU,OAAO,IAAK;AAAA,QACtB;AAAA,MACF;AACM,YAAA,eACJ,YAAO,MAAM,QAAQ,OAAO,MAAM,QAAQ,SAAS,CAAC,MAApD,mBAAuD;AACzD,UAAI,aAAa;AACf,yBAAiB,cAAc;AAAA,MAAA;AAEjC,uBAAiB,iBAAiB,QAAM,kBAAO,SAAQ,cAAf;AAC1B,oBAAA;AAEd,YAAM,IAAI,wBAAgC;AAC1C,2BAAqB,kBAAkB;AAAA,QACrC,MAAM;AAAA;AAAA,QAEN,SAAS,CAAC,sBAAsB,kBAAkB;AAAA,QAClD,aAAa,CAAC,MAAM,YAAY;AAC9B,gBAAM,aAAa,UAAU,GAAG,UAAU,gBAAgB,OAAO;AAC1D,iBAAA,UAAW,aAAa,MAAM,UAAU;AAAA,QACjD;AAAA,QACA,SAAS;AAAA,QACT,QAAQ,MAAM,EAAE,QAAQ,EAAE;AAAA,QAC1B,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG;AAAA,MAAA,CAC/B;AAEM,aAAA,UAAW,WAAW,MAAM,CAAC;AAAA,IACtC;AAAA,IACA,eAAe;AACN,aAAA;AAAA,IACT;AAAA,IACA,kBAAkB,CAAC,aAAa,UAAU,KAAK,QAAQ;AAAA,IACvD,mBAAmB,MAAM;AACvB,gBAAU,QAAQ,CAAC,MAAM,EAAA,CAAG;AAAA,IAAA;AAAA,EAEhC;AACF;"}
1
+ {"version":3,"file":"ssr-server.js","sources":["../../../src/ssr/ssr-server.ts"],"sourcesContent":["import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'\nimport { ReadableStreamPlugin } from 'seroval-plugins/web'\nimport invariant from 'tiny-invariant'\nimport { createControlledPromise } from '../utils'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { ShallowErrorPlugin } from './seroval-plugins'\nimport type { AnyRouter } from '../router'\nimport type { DehydratedMatch } from './ssr-client'\nimport type { DehydratedRouter } from './client'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest } from '../manifest'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n promise: Promise<string>\n }\n }\n}\n\nexport const GLOBAL_TSR = '$_TSR'\nconst SCOPE_ID = 'tsr'\n\nexport function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {\n const dehydratedMatch: DehydratedMatch = {\n i: match.id,\n u: match.updatedAt,\n s: match.status,\n }\n\n const properties = [\n ['__beforeLoadContext', 'b'],\n ['loaderData', 'l'],\n ['error', 'e'],\n ['ssr', 'ssr'],\n ] as const\n\n for (const [key, shorthand] of properties) {\n if (match[key] !== undefined) {\n dehydratedMatch[shorthand] = match[key]\n }\n }\n return dehydratedMatch\n}\n\nexport function attachRouterServerSsrUtils(\n router: AnyRouter,\n manifest: Manifest | undefined,\n) {\n router.ssr = {\n manifest,\n }\n const serializationRefs = new Map<unknown, number>()\n\n let initialScriptSent = false\n const getInitialScript = () => {\n if (initialScriptSent) {\n return ''\n }\n initialScriptSent = true\n return `${getCrossReferenceHeader(SCOPE_ID)};${minifiedTsrBootStrapScript};`\n }\n let _dehydrated = false\n const listeners: Array<() => void> = []\n\n router.serverSsr = {\n injectedHtml: [],\n injectHtml: (getHtml) => {\n const promise = Promise.resolve().then(getHtml)\n router.serverSsr!.injectedHtml.push(promise)\n router.emit({\n type: 'onInjectedHtml',\n promise,\n })\n\n return promise.then(() => {})\n },\n injectScript: (getScript) => {\n return router.serverSsr!.injectHtml(async () => {\n const script = await getScript()\n return `<script class='$tsr'>${getInitialScript()}${script};if (typeof $_TSR !== 'undefined') $_TSR.c()</script>`\n })\n },\n dehydrate: async () => {\n invariant(!_dehydrated, 'router is already dehydrated!')\n let matchesToDehydrate = router.state.matches\n if (router.isShell()) {\n // In SPA mode we only want to dehydrate the root match\n matchesToDehydrate = matchesToDehydrate.slice(0, 1)\n }\n const matches = matchesToDehydrate.map(dehydrateMatch)\n\n const dehydratedRouter: DehydratedRouter = {\n manifest: router.ssr!.manifest,\n matches,\n }\n const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id\n if (lastMatchId) {\n dehydratedRouter.lastMatchId = lastMatchId\n }\n dehydratedRouter.dehydratedData = await router.options.dehydrate?.()\n _dehydrated = true\n\n const p = createControlledPromise<string>()\n crossSerializeStream(dehydratedRouter, {\n refs: serializationRefs,\n // TODO make plugins configurable\n plugins: [ReadableStreamPlugin, ShallowErrorPlugin],\n onSerialize: (data, initial) => {\n const serialized = initial ? `${GLOBAL_TSR}[\"router\"]=` + data : data\n router.serverSsr!.injectScript(() => serialized)\n },\n scopeId: SCOPE_ID,\n onDone: () => p.resolve(''),\n onError: (err) => p.reject(err),\n })\n // make sure the stream is kept open until the promise is resolved\n router.serverSsr!.injectHtml(() => p)\n },\n isDehydrated() {\n return _dehydrated\n },\n onRenderFinished: (listener) => listeners.push(listener),\n setRenderFinished: () => {\n listeners.forEach((l) => l())\n },\n }\n}\n"],"names":[],"mappings":";;;;;;AAwBO,MAAM,aAAa;AAC1B,MAAM,WAAW;AAEV,SAAS,eAAe,OAAuC;AACpE,QAAM,kBAAmC;AAAA,IACvC,GAAG,MAAM;AAAA,IACT,GAAG,MAAM;AAAA,IACT,GAAG,MAAM;AAAA,EACX;AAEA,QAAM,aAAa;AAAA,IACjB,CAAC,uBAAuB,GAAG;AAAA,IAC3B,CAAC,cAAc,GAAG;AAAA,IAClB,CAAC,SAAS,GAAG;AAAA,IACb,CAAC,OAAO,KAAK;AAAA,EACf;AAEA,aAAW,CAAC,KAAK,SAAS,KAAK,YAAY;AACrC,QAAA,MAAM,GAAG,MAAM,QAAW;AACZ,sBAAA,SAAS,IAAI,MAAM,GAAG;AAAA,IAAA;AAAA,EACxC;AAEK,SAAA;AACT;AAEgB,SAAA,2BACd,QACA,UACA;AACA,SAAO,MAAM;AAAA,IACX;AAAA,EACF;AACM,QAAA,wCAAwB,IAAqB;AAEnD,MAAI,oBAAoB;AACxB,QAAM,mBAAmB,MAAM;AAC7B,QAAI,mBAAmB;AACd,aAAA;AAAA,IAAA;AAEW,wBAAA;AACpB,WAAO,GAAG,wBAAwB,QAAQ,CAAC,IAAI,0BAA0B;AAAA,EAC3E;AACA,MAAI,cAAc;AAClB,QAAM,YAA+B,CAAC;AAEtC,SAAO,YAAY;AAAA,IACjB,cAAc,CAAC;AAAA,IACf,YAAY,CAAC,YAAY;AACvB,YAAM,UAAU,QAAQ,QAAQ,EAAE,KAAK,OAAO;AACvC,aAAA,UAAW,aAAa,KAAK,OAAO;AAC3C,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN;AAAA,MAAA,CACD;AAEM,aAAA,QAAQ,KAAK,MAAM;AAAA,MAAA,CAAE;AAAA,IAC9B;AAAA,IACA,cAAc,CAAC,cAAc;AACpB,aAAA,OAAO,UAAW,WAAW,YAAY;AACxC,cAAA,SAAS,MAAM,UAAU;AAC/B,eAAO,wBAAwB,iBAAkB,CAAA,GAAG,MAAM;AAAA,MAAA,CAC3D;AAAA,IACH;AAAA,IACA,WAAW,YAAY;;AACX,gBAAA,CAAC,aAAa,+BAA+B;AACnD,UAAA,qBAAqB,OAAO,MAAM;AAClC,UAAA,OAAO,WAAW;AAEC,6BAAA,mBAAmB,MAAM,GAAG,CAAC;AAAA,MAAA;AAE9C,YAAA,UAAU,mBAAmB,IAAI,cAAc;AAErD,YAAM,mBAAqC;AAAA,QACzC,UAAU,OAAO,IAAK;AAAA,QACtB;AAAA,MACF;AACA,YAAM,eAAc,wBAAmB,mBAAmB,SAAS,CAAC,MAAhD,mBAAmD;AACvE,UAAI,aAAa;AACf,yBAAiB,cAAc;AAAA,MAAA;AAEjC,uBAAiB,iBAAiB,QAAM,kBAAO,SAAQ,cAAf;AAC1B,oBAAA;AAEd,YAAM,IAAI,wBAAgC;AAC1C,2BAAqB,kBAAkB;AAAA,QACrC,MAAM;AAAA;AAAA,QAEN,SAAS,CAAC,sBAAsB,kBAAkB;AAAA,QAClD,aAAa,CAAC,MAAM,YAAY;AAC9B,gBAAM,aAAa,UAAU,GAAG,UAAU,gBAAgB,OAAO;AAC1D,iBAAA,UAAW,aAAa,MAAM,UAAU;AAAA,QACjD;AAAA,QACA,SAAS;AAAA,QACT,QAAQ,MAAM,EAAE,QAAQ,EAAE;AAAA,QAC1B,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG;AAAA,MAAA,CAC/B;AAEM,aAAA,UAAW,WAAW,MAAM,CAAC;AAAA,IACtC;AAAA,IACA,eAAe;AACN,aAAA;AAAA,IACT;AAAA,IACA,kBAAkB,CAAC,aAAa,UAAU,KAAK,QAAQ;AAAA,IACvD,mBAAmB,MAAM;AACvB,gBAAU,QAAQ,CAAC,MAAM,EAAA,CAAG;AAAA,IAAA;AAAA,EAEhC;AACF;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
- "version": "1.127.0",
3
+ "version": "1.127.3",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -66,7 +66,6 @@
66
66
  "dependencies": {
67
67
  "@tanstack/store": "^0.7.0",
68
68
  "cookie-es": "^1.2.2",
69
- "jsesc": "^3.1.0",
70
69
  "seroval": "^1.3.2",
71
70
  "seroval-plugins": "^1.3.2",
72
71
  "tiny-invariant": "^1.3.3",
@@ -74,7 +73,6 @@
74
73
  "@tanstack/history": "1.121.34"
75
74
  },
76
75
  "devDependencies": {
77
- "@types/jsesc": "^3.0.3",
78
76
  "esbuild": "^0.25.0"
79
77
  },
80
78
  "scripts": {}
package/src/router.ts CHANGED
@@ -341,6 +341,11 @@ export interface RouterOptions<
341
341
  */
342
342
  isServer?: boolean
343
343
 
344
+ /**
345
+ * @default false
346
+ */
347
+ isShell?: boolean
348
+
344
349
  /**
345
350
  * The default `ssr` a route should use if no `ssr` is provided.
346
351
  *
@@ -813,7 +818,9 @@ export class RouterCore<
813
818
  // router can be used in a non-react environment if necessary
814
819
  startTransition: StartTransitionFn = (fn) => fn()
815
820
 
816
- isShell = false
821
+ isShell() {
822
+ return this.options.isShell
823
+ }
817
824
 
818
825
  update: UpdateFn<
819
826
  TRouteTree,
@@ -903,10 +910,6 @@ export class RouterCore<
903
910
  'selector(:active-view-transition-type(a)',
904
911
  )
905
912
  }
906
-
907
- if ((this.latestLocation.search as any).__TSS_SHELL) {
908
- this.isShell = true
909
- }
910
913
  }
911
914
 
912
915
  get state() {
@@ -1787,11 +1790,7 @@ export class RouterCore<
1787
1790
  }
1788
1791
  }
1789
1792
  // Match the routes
1790
- let pendingMatches = this.matchRoutes(this.latestLocation)
1791
- // in SPA mode we only want to load the root route
1792
- if (this.isShell) {
1793
- pendingMatches = pendingMatches.slice(0, 1)
1794
- }
1793
+ const pendingMatches = this.matchRoutes(this.latestLocation)
1795
1794
 
1796
1795
  // Ingest the new matches
1797
1796
  this.__store.setState((s) => ({
@@ -2064,40 +2063,6 @@ export class RouterCore<
2064
2063
  const triggerOnReady = async () => {
2065
2064
  if (!rendered) {
2066
2065
  rendered = true
2067
-
2068
- // create a minPendingPromise for matches that have forcePending set to true
2069
- // usually the minPendingPromise is created in the Match component if a pending match is rendered
2070
- // however, this might be too late if the match synchronously resolves
2071
- if (!allPreload && !this.isServer) {
2072
- matches.forEach((match) => {
2073
- const {
2074
- id: matchId,
2075
- routeId,
2076
- _forcePending,
2077
- minPendingPromise,
2078
- } = match
2079
- const route = this.looseRoutesById[routeId]!
2080
- const pendingMinMs =
2081
- route.options.pendingMinMs ?? this.options.defaultPendingMinMs
2082
- if (_forcePending && pendingMinMs && !minPendingPromise) {
2083
- const minPendingPromise = createControlledPromise<void>()
2084
- updateMatch(matchId, (prev) => ({
2085
- ...prev,
2086
- minPendingPromise,
2087
- }))
2088
-
2089
- setTimeout(() => {
2090
- minPendingPromise.resolve()
2091
- // We've handled the minPendingPromise, so we can delete it
2092
- updateMatch(matchId, (prev) => ({
2093
- ...prev,
2094
- minPendingPromise: undefined,
2095
- }))
2096
- }, pendingMinMs)
2097
- }
2098
- })
2099
- }
2100
-
2101
2066
  await onReady?.()
2102
2067
  }
2103
2068
  }
@@ -2234,50 +2199,55 @@ export class RouterCore<
2234
2199
 
2235
2200
  // on the server, determine whether SSR the current match or not
2236
2201
  if (this.isServer) {
2237
- const defaultSsr = this.options.defaultSsr ?? true
2238
2202
  let ssr: boolean | 'data-only'
2239
- if (parentMatch?.ssr === false) {
2240
- ssr = false
2203
+ // in SPA mode, only SSR the root route
2204
+ if (this.isShell()) {
2205
+ ssr = matchId === rootRouteId
2241
2206
  } else {
2242
- let tempSsr: boolean | 'data-only'
2243
- if (route.options.ssr === undefined) {
2244
- tempSsr = defaultSsr
2245
- } else if (typeof route.options.ssr === 'function') {
2246
- const { search, params } = this.getMatch(matchId)!
2247
-
2248
- function makeMaybe(value: any, error: any) {
2249
- if (error) {
2250
- return { status: 'error' as const, error }
2207
+ const defaultSsr = this.options.defaultSsr ?? true
2208
+ if (parentMatch?.ssr === false) {
2209
+ ssr = false
2210
+ } else {
2211
+ let tempSsr: boolean | 'data-only'
2212
+ if (route.options.ssr === undefined) {
2213
+ tempSsr = defaultSsr
2214
+ } else if (typeof route.options.ssr === 'function') {
2215
+ const { search, params } = this.getMatch(matchId)!
2216
+
2217
+ function makeMaybe(value: any, error: any) {
2218
+ if (error) {
2219
+ return { status: 'error' as const, error }
2220
+ }
2221
+ return { status: 'success' as const, value }
2251
2222
  }
2252
- return { status: 'success' as const, value }
2253
- }
2254
2223
 
2255
- const ssrFnContext: SsrContextOptions<any, any, any> = {
2256
- search: makeMaybe(search, existingMatch.searchError),
2257
- params: makeMaybe(params, existingMatch.paramsError),
2258
- location,
2259
- matches: matches.map((match) => ({
2260
- index: match.index,
2261
- pathname: match.pathname,
2262
- fullPath: match.fullPath,
2263
- staticData: match.staticData,
2264
- id: match.id,
2265
- routeId: match.routeId,
2266
- search: makeMaybe(match.search, match.searchError),
2267
- params: makeMaybe(match.params, match.paramsError),
2268
- ssr: match.ssr,
2269
- })),
2224
+ const ssrFnContext: SsrContextOptions<any, any, any> = {
2225
+ search: makeMaybe(search, existingMatch.searchError),
2226
+ params: makeMaybe(params, existingMatch.paramsError),
2227
+ location,
2228
+ matches: matches.map((match) => ({
2229
+ index: match.index,
2230
+ pathname: match.pathname,
2231
+ fullPath: match.fullPath,
2232
+ staticData: match.staticData,
2233
+ id: match.id,
2234
+ routeId: match.routeId,
2235
+ search: makeMaybe(match.search, match.searchError),
2236
+ params: makeMaybe(match.params, match.paramsError),
2237
+ ssr: match.ssr,
2238
+ })),
2239
+ }
2240
+ tempSsr =
2241
+ (await route.options.ssr(ssrFnContext)) ?? defaultSsr
2242
+ } else {
2243
+ tempSsr = route.options.ssr
2270
2244
  }
2271
- tempSsr =
2272
- (await route.options.ssr(ssrFnContext)) ?? defaultSsr
2273
- } else {
2274
- tempSsr = route.options.ssr
2275
- }
2276
2245
 
2277
- if (tempSsr === true && parentMatch?.ssr === 'data-only') {
2278
- ssr = 'data-only'
2279
- } else {
2280
- ssr = tempSsr
2246
+ if (tempSsr === true && parentMatch?.ssr === 'data-only') {
2247
+ ssr = 'data-only'
2248
+ } else {
2249
+ ssr = tempSsr
2250
+ }
2281
2251
  }
2282
2252
  }
2283
2253
  updateMatch(matchId, (prev) => ({
@@ -2505,8 +2475,6 @@ export class RouterCore<
2505
2475
  ...head,
2506
2476
  }))
2507
2477
  return this.getMatch(matchId)!
2508
- } else {
2509
- await potentialPendingMinPromise()
2510
2478
  }
2511
2479
  }
2512
2480
  // there is a loaderPromise, so we are in the middle of a load
@@ -2678,18 +2646,13 @@ export class RouterCore<
2678
2646
  }
2679
2647
 
2680
2648
  // If the route is successful and still fresh, just resolve
2681
- const { status, invalid, _forcePending } =
2682
- this.getMatch(matchId)!
2649
+ const { status, invalid } = this.getMatch(matchId)!
2683
2650
  loaderShouldRunAsync =
2684
2651
  status === 'success' &&
2685
2652
  (invalid || (shouldReload ?? age > staleAge))
2686
2653
  if (preload && route.options.preload === false) {
2687
2654
  // Do nothing
2688
- } else if (
2689
- loaderShouldRunAsync &&
2690
- !sync &&
2691
- !_forcePending
2692
- ) {
2655
+ } else if (loaderShouldRunAsync && !sync) {
2693
2656
  loaderIsRunningAsync = true
2694
2657
  ;(async () => {
2695
2658
  try {
@@ -2714,9 +2677,6 @@ export class RouterCore<
2714
2677
  ) {
2715
2678
  await runLoader()
2716
2679
  } else {
2717
- if (_forcePending) {
2718
- await potentialPendingMinPromise()
2719
- }
2720
2680
  // if the loader did not run, still update head.
2721
2681
  // reason: parent's beforeLoad may have changed the route context
2722
2682
  // and only now do we know the route context (and that the loader would not run)
@@ -2747,7 +2707,6 @@ export class RouterCore<
2747
2707
  invalid: false,
2748
2708
  pendingTimeout: undefined,
2749
2709
  _dehydrated: undefined,
2750
- _forcePending: undefined,
2751
2710
  }
2752
2711
  })
2753
2712
  return this.getMatch(matchId)!
@@ -1,5 +1,7 @@
1
1
  import invariant from 'tiny-invariant'
2
- import type { MakeRouteMatch } from '../Matches'
2
+ import { batch } from '@tanstack/store'
3
+ import { createControlledPromise } from '../utils'
4
+ import type { AnyRouteMatch, MakeRouteMatch } from '../Matches'
3
5
  import type { AnyRouter } from '../router'
4
6
  import type { Manifest } from '../manifest'
5
7
  import type { RouteContextOptions } from '../route'
@@ -70,6 +72,29 @@ export async function hydrate(router: AnyRouter): Promise<any> {
70
72
  }),
71
73
  )
72
74
 
75
+ function setMatchForcePending(match: AnyRouteMatch) {
76
+ // usually the minPendingPromise is created in the Match component if a pending match is rendered
77
+ // however, this might be too late if the match synchronously resolves
78
+ const route = router.looseRoutesById[match.routeId]!
79
+ const pendingMinMs =
80
+ route.options.pendingMinMs ?? router.options.defaultPendingMinMs
81
+ if (pendingMinMs) {
82
+ const minPendingPromise = createControlledPromise<void>()
83
+ match.minPendingPromise = minPendingPromise
84
+ match._forcePending = true
85
+
86
+ setTimeout(() => {
87
+ minPendingPromise.resolve()
88
+ // We've handled the minPendingPromise, so we can delete it
89
+ router.updateMatch(match.id, (prev) => ({
90
+ ...prev,
91
+ minPendingPromise: undefined,
92
+ _forcePending: undefined,
93
+ }))
94
+ }, pendingMinMs)
95
+ }
96
+ }
97
+
73
98
  // Right after hydration and before the first render, we need to rehydrate each match
74
99
  // First step is to reyhdrate loaderData and __beforeLoadContext
75
100
  let firstNonSsrMatchIndex: number | undefined = undefined
@@ -93,13 +118,9 @@ export async function hydrate(router: AnyRouter): Promise<any> {
93
118
  if (match.ssr === 'data-only' || match.ssr === false) {
94
119
  if (firstNonSsrMatchIndex === undefined) {
95
120
  firstNonSsrMatchIndex = match.index
96
- match._forcePending = true
121
+ setMatchForcePending(match)
97
122
  }
98
123
  }
99
-
100
- if (match.ssr === false) {
101
- return
102
- }
103
124
  })
104
125
 
105
126
  router.__store.setState((s) => {
@@ -163,6 +184,17 @@ export async function hydrate(router: AnyRouter): Promise<any> {
163
184
  }),
164
185
  )
165
186
 
187
+ const isSpaMode = matches[matches.length - 1]!.id !== lastMatchId
188
+ const hasSsrFalseMatches = matches.some((m) => m.ssr === false)
189
+ // all matches have data from the server and we are not in SPA mode so we don't need to kick of router.load()
190
+ if (!hasSsrFalseMatches && !isSpaMode) {
191
+ matches.forEach((match) => {
192
+ // remove the _dehydrate flag since we won't run router.load() which would remove it
193
+ match._dehydrated = undefined
194
+ })
195
+ return routeChunkPromise
196
+ }
197
+
166
198
  // schedule router.load() to run after the next tick so we can store the promise in the match before loading starts
167
199
  const loadPromise = Promise.resolve()
168
200
  .then(() => router.load())
@@ -170,27 +202,39 @@ export async function hydrate(router: AnyRouter): Promise<any> {
170
202
  console.error('Error during router hydration:', err)
171
203
  })
172
204
 
173
- // in SPA mode we need to keep the outermost match pending until router.load() is finished
205
+ // in SPA mode we need to keep the first match below the root route pending until router.load() is finished
174
206
  // this will prevent that other pending components are rendered but hydration is not blocked
175
- if (matches[matches.length - 1]!.id !== lastMatchId) {
176
- const matchId = matches[0]!.id
177
- router.updateMatch(matchId, (prev) => {
178
- return {
179
- ...prev,
180
- _displayPending: true,
181
- displayPendingPromise: loadPromise,
182
- // make sure that the pending component is displayed for at least pendingMinMs
183
- _forcePending: true,
184
- }
185
- })
186
- // hide the pending component once the load is finished
207
+ if (isSpaMode) {
208
+ const match = matches[1]
209
+ invariant(
210
+ match,
211
+ 'Expected to find a match below the root match in SPA mode.',
212
+ )
213
+ setMatchForcePending(match)
214
+
215
+ match._displayPending = true
216
+ match.displayPendingPromise = loadPromise
217
+
187
218
  loadPromise.then(() => {
188
- router.updateMatch(matchId, (prev) => {
189
- return {
190
- ...prev,
191
- _displayPending: undefined,
192
- displayPendingPromise: undefined,
219
+ batch(() => {
220
+ // ensure router is not in status 'pending' anymore
221
+ // this usually happens in Transitioner but if loading synchronously resolves,
222
+ // Transitioner won't be rendered while loading so it cannot track the change from loading:true to loading:false
223
+ if (router.__store.state.status === 'pending') {
224
+ router.__store.setState((s) => ({
225
+ ...s,
226
+ status: 'idle',
227
+ resolvedLocation: s.location,
228
+ }))
193
229
  }
230
+ // hide the pending component once the load is finished
231
+ router.updateMatch(match.id, (prev) => {
232
+ return {
233
+ ...prev,
234
+ _displayPending: undefined,
235
+ displayPendingPromise: undefined,
236
+ }
237
+ })
194
238
  })
195
239
  })
196
240
  }
@@ -87,14 +87,18 @@ export function attachRouterServerSsrUtils(
87
87
  },
88
88
  dehydrate: async () => {
89
89
  invariant(!_dehydrated, 'router is already dehydrated!')
90
- const matches = router.state.matches.map(dehydrateMatch)
90
+ let matchesToDehydrate = router.state.matches
91
+ if (router.isShell()) {
92
+ // In SPA mode we only want to dehydrate the root match
93
+ matchesToDehydrate = matchesToDehydrate.slice(0, 1)
94
+ }
95
+ const matches = matchesToDehydrate.map(dehydrateMatch)
91
96
 
92
97
  const dehydratedRouter: DehydratedRouter = {
93
98
  manifest: router.ssr!.manifest,
94
99
  matches,
95
100
  }
96
- const lastMatchId =
97
- router.state.matches[router.state.matches.length - 1]?.id
101
+ const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id
98
102
  if (lastMatchId) {
99
103
  dehydratedRouter.lastMatchId = lastMatchId
100
104
  }