diff --git a/labeler-config.yml b/labeler-config.yml index 69529887a6e..15ee4ba018e 100644 --- a/labeler-config.yml +++ b/labeler-config.yml @@ -43,6 +43,9 @@ 'package: router-generator': - changed-files: - any-glob-to-any-file: 'packages/router-generator/**/*' +'package: router-is-server': + - changed-files: + - any-glob-to-any-file: 'packages/router-is-server/**/*' 'package: router-plugin': - changed-files: - any-glob-to-any-file: 'packages/router-plugin/**/*' diff --git a/packages/react-router/src/Asset.tsx b/packages/react-router/src/Asset.tsx index ce1a9699e47..ef82d6637ae 100644 --- a/packages/react-router/src/Asset.tsx +++ b/packages/react-router/src/Asset.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { isServer } from '@tanstack/router-core' import { useRouter } from './useRouter' import type { RouterManagedTag } from '@tanstack/router-core' @@ -143,7 +144,7 @@ function Script({ return undefined }, [attrs, children]) - if (!router.isServer) { + if (!(isServer ?? router.isServer)) { const { src, ...rest } = attrs || {} // render an empty script on the client just to avoid hydration errors return ( diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx index a0c67c2f0a7..e50f9b7b7d1 100644 --- a/packages/react-router/src/Match.tsx +++ b/packages/react-router/src/Match.tsx @@ -6,6 +6,7 @@ import { getLocationChangeInfo, isNotFound, isRedirect, + isServer, rootRouteId, } from '@tanstack/router-core' import { CatchBoundary, ErrorComponent } from './CatchBoundary' @@ -246,7 +247,7 @@ export const MatchInner = React.memo(function MatchInnerImpl({ const routerMatch = router.getMatch(match.id) if (routerMatch && !routerMatch._nonReactive.minPendingPromise) { // Create a promise that will resolve after the minPendingMs - if (!router.isServer) { + if (!(isServer ?? router.isServer)) { const minPendingPromise = createControlledPromise() routerMatch._nonReactive.minPendingPromise = minPendingPromise @@ -285,7 +286,7 @@ export const MatchInner = React.memo(function MatchInnerImpl({ // of a suspense boundary. This is the only way to get // renderToPipeableStream to not hang indefinitely. // We'll serialize the error and rethrow it on the client. - if (router.isServer) { + if (isServer ?? router.isServer) { const RouteErrorComponent = (route.options.errorComponent ?? router.options.defaultErrorComponent) || diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx index 0d64831d944..8c1b661b866 100644 --- a/packages/react-router/src/Matches.tsx +++ b/packages/react-router/src/Matches.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import warning from 'tiny-warning' -import { rootRouteId } from '@tanstack/router-core' +import { isServer, rootRouteId } from '@tanstack/router-core' import { CatchBoundary, ErrorComponent } from './CatchBoundary' import { useRouterState } from './useRouterState' import { useRouter } from './useRouter' @@ -56,13 +56,14 @@ export function Matches() { // Do not render a root Suspense during SSR or hydrating from SSR const ResolvedSuspense = - router.isServer || (typeof document !== 'undefined' && router.ssr) + (isServer ?? router.isServer) || + (typeof document !== 'undefined' && router.ssr) ? SafeFragment : React.Suspense const inner = ( - {!router.isServer && } + {!(isServer ?? router.isServer) && } ) diff --git a/packages/react-router/src/ScriptOnce.tsx b/packages/react-router/src/ScriptOnce.tsx index 97609c6fc7b..c37bb51a30d 100644 --- a/packages/react-router/src/ScriptOnce.tsx +++ b/packages/react-router/src/ScriptOnce.tsx @@ -1,3 +1,4 @@ +import { isServer } from '@tanstack/router-core' import { useRouter } from './useRouter' /** @@ -5,7 +6,7 @@ import { useRouter } from './useRouter' */ export function ScriptOnce({ children }: { children: string }) { const router = useRouter() - if (!router.isServer) { + if (!(isServer ?? router.isServer)) { return null } diff --git a/packages/react-router/src/scroll-restoration.tsx b/packages/react-router/src/scroll-restoration.tsx index 8430f8182e0..8a88e9a6711 100644 --- a/packages/react-router/src/scroll-restoration.tsx +++ b/packages/react-router/src/scroll-restoration.tsx @@ -1,6 +1,7 @@ import { defaultGetScrollRestorationKey, escapeHtml, + isServer, restoreScroll, storageKey, } from '@tanstack/router-core' @@ -9,7 +10,7 @@ import { ScriptOnce } from './ScriptOnce' export function ScrollRestoration() { const router = useRouter() - if (!router.isScrollRestoring || !router.isServer) { + if (!router.isScrollRestoring || !(isServer ?? router.isServer)) { return null } if (typeof router.options.scrollRestoration === 'function') { diff --git a/packages/react-router/vite.config.ts b/packages/react-router/vite.config.ts index d5ddc898cf5..4ed56483684 100644 --- a/packages/react-router/vite.config.ts +++ b/packages/react-router/vite.config.ts @@ -5,6 +5,13 @@ import packageJson from './package.json' const config = defineConfig({ plugins: [react()], + // Add 'development' condition for tests to resolve @tanstack/router-is-server + // to the development export (isServer = undefined) instead of node (isServer = true) + ...(process.env.VITEST && { + resolve: { + conditions: ['development'], + }, + }), test: { name: packageJson.name, dir: './tests', diff --git a/packages/router-core/package.json b/packages/router-core/package.json index 3302a13be41..07b1efa70d3 100644 --- a/packages/router-core/package.json +++ b/packages/router-core/package.json @@ -80,6 +80,7 @@ }, "dependencies": { "@tanstack/history": "workspace:*", + "@tanstack/router-is-server": "workspace:*", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.2", diff --git a/packages/router-core/src/index.ts b/packages/router-core/src/index.ts index 1897f078801..5fe3a1af743 100644 --- a/packages/router-core/src/index.ts +++ b/packages/router-core/src/index.ts @@ -105,6 +105,7 @@ export { export { encode, decode } from './qss' export { rootRouteId } from './root' export type { RootRouteId } from './root' +export { isServer } from './isServer' export { BaseRoute, BaseRouteApi, BaseRootRoute } from './route' export type { diff --git a/packages/router-core/src/isServer.ts b/packages/router-core/src/isServer.ts new file mode 100644 index 00000000000..560947269ac --- /dev/null +++ b/packages/router-core/src/isServer.ts @@ -0,0 +1,24 @@ +/** + * Static server/client detection for tree-shaking support. + * + * This file re-exports `isServer` from `@tanstack/router-is-server` which uses + * conditional exports to provide different values based on the environment: + * + * - `browser` condition → `false` (client) + * - `node`/`worker`/`deno`/`bun` → `true` (server) + * - `development` condition → `undefined` (for tests, falls back to router.isServer) + * + * The bundler resolves the correct file at build time based on export conditions, + * and since the value is a literal constant, dead code can be eliminated. + * + * @example + * ```typescript + * import { isServer } from '@tanstack/router-core' + * + * // The ?? operator provides fallback for development/test mode + * if (isServer ?? router.isServer) { + * // Server-only code - eliminated in client bundles + * } + * ``` + */ +export { isServer } from '@tanstack/router-is-server' diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts index d9b1d622647..e524032878b 100644 --- a/packages/router-core/src/load-matches.ts +++ b/packages/router-core/src/load-matches.ts @@ -4,6 +4,7 @@ import { createControlledPromise, isPromise } from './utils' import { isNotFound } from './not-found' import { rootRouteId } from './root' import { isRedirect } from './redirect' +import { isServer } from './isServer' import type { NotFoundError } from './not-found' import type { ParsedLocation } from './location' import type { @@ -169,11 +170,11 @@ const shouldSkipLoader = ( ): boolean => { const match = inner.router.getMatch(matchId)! // upon hydration, we skip the loader if the match has been dehydrated on the server - if (!inner.router.isServer && match._nonReactive.dehydrated) { + if (!(isServer ?? inner.router.isServer) && match._nonReactive.dehydrated) { return true } - if (inner.router.isServer && match.ssr === false) { + if ((isServer ?? inner.router.isServer) && match.ssr === false) { return true } @@ -306,7 +307,7 @@ const setupPendingTimeout = ( route.options.pendingMs ?? inner.router.options.defaultPendingMs const shouldPending = !!( inner.onReady && - !inner.router.isServer && + !(isServer ?? inner.router.isServer) && !resolvePreload(inner, matchId) && (route.options.loader || route.options.beforeLoad || @@ -520,7 +521,7 @@ const handleBeforeLoad = ( const serverSsr = () => { // on the server, determine whether SSR the current match or not - if (inner.router.isServer) { + if (isServer ?? inner.router.isServer) { const maybePromise = isBeforeLoadSsr(inner, matchId, index, route) if (isPromise(maybePromise)) return maybePromise.then(queueExecution) } @@ -635,7 +636,7 @@ const runLoader = async ( // Actually run the loader and handle the result try { - if (!inner.router.isServer || match.ssr === true) { + if (!(isServer ?? inner.router.isServer) || match.ssr === true) { loadRouteChunk(route) } @@ -759,7 +760,7 @@ const loadRouteMatch = async ( const route = inner.router.looseRoutesById[routeId]! if (shouldSkipLoader(inner, matchId)) { - if (inner.router.isServer) { + if (isServer ?? inner.router.isServer) { return inner.router.getMatch(matchId)! } } else { @@ -879,7 +880,7 @@ export async function loadMatches(arg: { // make sure the pending component is immediately rendered when hydrating a match that is not SSRed // the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached if ( - !inner.router.isServer && + !(isServer ?? inner.router.isServer) && inner.router.state.matches.some((d) => d._forcePending) ) { triggerOnReady(inner) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index b7a8defc186..7e0dd0661f6 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -25,6 +25,7 @@ import { trimPath, trimPathRight, } from './path' +import { isServer } from './isServer' import { createLRUCache } from './lru-cache' import { isNotFound } from './not-found' import { setupScrollRestoration } from './scroll-restoration' @@ -1018,7 +1019,7 @@ export class RouterCore< (this.options.history && this.options.history !== this.history) ) { if (!this.options.history) { - if (!this.isServer) { + if (!(isServer ?? this.isServer)) { this.history = createBrowserHistory() as TRouterHistory } } else { @@ -1028,7 +1029,11 @@ export class RouterCore< this.origin = this.options.origin if (!this.origin) { - if (!this.isServer && window?.origin && window.origin !== 'null') { + if ( + !(isServer ?? this.isServer) && + window?.origin && + window.origin !== 'null' + ) { this.origin = window.origin } else { // fallback for the server, can be overridden by calling router.update({origin}) on the server @@ -1044,7 +1049,7 @@ export class RouterCore< this.routeTree = this.options.routeTree as TRouteTree let processRouteTreeResult: ProcessRouteTreeResult if ( - this.isServer && + (isServer ?? this.isServer) && globalThis.__TSR_CACHE__ && globalThis.__TSR_CACHE__.routeTree === this.routeTree ) { @@ -1055,7 +1060,10 @@ export class RouterCore< this.resolvePathCache = createLRUCache(1000) processRouteTreeResult = this.buildRouteTree() // only cache if nothing else is cached yet - if (this.isServer && globalThis.__TSR_CACHE__ === undefined) { + if ( + (isServer ?? this.isServer) && + globalThis.__TSR_CACHE__ === undefined + ) { globalThis.__TSR_CACHE__ = { routeTree: this.routeTree, processRouteTreeResult: processRouteTreeResult as any, @@ -1479,7 +1487,7 @@ export class RouterCore< match = { id: matchId, - ssr: this.isServer ? undefined : route.options.ssr, + ssr: (isServer ?? this.isServer) ? undefined : route.options.ssr, index, routeId: route.id, params: previousMatch @@ -2215,7 +2223,7 @@ export class RouterCore< this.cancelMatches() this.updateLatestLocation() - if (this.isServer) { + if (isServer ?? this.isServer) { // for SPAs on the initial load, this is handled by the Transitioner const nextLocation = this.buildLocation({ to: this.latestLocation.pathname, @@ -2365,7 +2373,7 @@ export class RouterCore< } catch (err) { if (isRedirect(err)) { redirect = err - if (!this.isServer) { + if (!(isServer ?? this.isServer)) { this.navigate({ ...redirect.options, replace: true, diff --git a/packages/router-core/src/scroll-restoration.ts b/packages/router-core/src/scroll-restoration.ts index c8614429a9b..fbc5eb9c589 100644 --- a/packages/router-core/src/scroll-restoration.ts +++ b/packages/router-core/src/scroll-restoration.ts @@ -1,4 +1,5 @@ import { functionalUpdate } from './utils' +import { isServer } from './isServer' import type { AnyRouter } from './router' import type { ParsedLocation } from './location' import type { NonNullableUpdater } from './utils' @@ -217,7 +218,7 @@ export function restoreScroll({ /** Setup global listeners and hooks to support scroll restoration. */ /** Setup global listeners and hooks to support scroll restoration. */ export function setupScrollRestoration(router: AnyRouter, force?: boolean) { - if (!scrollRestorationCache && !router.isServer) { + if (!scrollRestorationCache && !(isServer ?? router.isServer)) { return } const shouldScrollRestoration = @@ -228,7 +229,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) { } if ( - router.isServer || + (isServer ?? router.isServer) || router.isScrollRestorationSetup || !scrollRestorationCache ) { diff --git a/packages/router-is-server/eslint.config.js b/packages/router-is-server/eslint.config.js new file mode 100644 index 00000000000..b527b96a78a --- /dev/null +++ b/packages/router-is-server/eslint.config.js @@ -0,0 +1,15 @@ +// @ts-check + +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + rules: { + // We explicitly annotate `isServer: boolean` even though it's inferrable + // because the explicit type is important for documentation and ensures + // consistent typing across all conditional export files + '@typescript-eslint/no-inferrable-types': 'off', + }, + }, +] diff --git a/packages/router-is-server/package.json b/packages/router-is-server/package.json new file mode 100644 index 00000000000..5cd2fe56788 --- /dev/null +++ b/packages/router-is-server/package.json @@ -0,0 +1,133 @@ +{ + "name": "@tanstack/router-is-server", + "version": "1.127.3", + "description": "Server/client detection for TanStack Router with tree-shaking support", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/router.git", + "directory": "packages/router-is-server" + }, + "homepage": "https://tanstack.com/router", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "router", + "server", + "client", + "tree-shaking" + ], + "scripts": { + "clean": "rimraf ./dist && rimraf ./coverage", + "test:eslint": "eslint ./src", + "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", + "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js", + "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js", + "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js", + "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js", + "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js", + "test:types:ts59": "tsc", + "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", + "test:unit": "exit 0", + "test:unit:dev": "exit 0", + "build": "vite build" + }, + "type": "module", + "types": "dist/esm/server.d.ts", + "main": "dist/cjs/server.cjs", + "module": "dist/esm/server.js", + "exports": { + ".": { + "development": { + "import": { + "types": "./dist/esm/development.d.ts", + "default": "./dist/esm/development.js" + }, + "require": { + "types": "./dist/cjs/development.d.cts", + "default": "./dist/cjs/development.cjs" + } + }, + "workerd": { + "import": { + "types": "./dist/esm/server.d.ts", + "default": "./dist/esm/server.js" + }, + "require": { + "types": "./dist/cjs/server.d.cts", + "default": "./dist/cjs/server.cjs" + } + }, + "worker": { + "import": { + "types": "./dist/esm/server.d.ts", + "default": "./dist/esm/server.js" + }, + "require": { + "types": "./dist/cjs/server.d.cts", + "default": "./dist/cjs/server.cjs" + } + }, + "browser": { + "import": { + "types": "./dist/esm/client.d.ts", + "default": "./dist/esm/client.js" + }, + "require": { + "types": "./dist/cjs/client.d.cts", + "default": "./dist/cjs/client.cjs" + } + }, + "deno": { + "import": { + "types": "./dist/esm/server.d.ts", + "default": "./dist/esm/server.js" + }, + "require": { + "types": "./dist/cjs/server.d.cts", + "default": "./dist/cjs/server.cjs" + } + }, + "node": { + "import": { + "types": "./dist/esm/server.d.ts", + "default": "./dist/esm/server.js" + }, + "require": { + "types": "./dist/cjs/server.d.cts", + "default": "./dist/cjs/server.cjs" + } + }, + "bun": { + "import": { + "types": "./dist/esm/server.d.ts", + "default": "./dist/esm/server.js" + }, + "require": { + "types": "./dist/cjs/server.d.cts", + "default": "./dist/cjs/server.cjs" + } + }, + "import": { + "types": "./dist/esm/client.d.ts", + "default": "./dist/esm/client.js" + }, + "require": { + "types": "./dist/cjs/client.d.cts", + "default": "./dist/cjs/client.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=12" + } +} diff --git a/packages/router-is-server/src/client.ts b/packages/router-is-server/src/client.ts new file mode 100644 index 00000000000..4023ccd1648 --- /dev/null +++ b/packages/router-is-server/src/client.ts @@ -0,0 +1 @@ +export const isServer: boolean = false diff --git a/packages/router-is-server/src/development.ts b/packages/router-is-server/src/development.ts new file mode 100644 index 00000000000..3632fabf055 --- /dev/null +++ b/packages/router-is-server/src/development.ts @@ -0,0 +1,2 @@ +// Development/test mode - returns undefined so fallback to router.isServer is used +export const isServer: boolean | undefined = undefined diff --git a/packages/router-is-server/src/server.ts b/packages/router-is-server/src/server.ts new file mode 100644 index 00000000000..f5608b94c61 --- /dev/null +++ b/packages/router-is-server/src/server.ts @@ -0,0 +1 @@ +export const isServer: boolean = true diff --git a/packages/router-is-server/tsconfig.json b/packages/router-is-server/tsconfig.json new file mode 100644 index 00000000000..596e2cf729a --- /dev/null +++ b/packages/router-is-server/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"] +} diff --git a/packages/router-is-server/vite.config.ts b/packages/router-is-server/vite.config.ts new file mode 100644 index 00000000000..2cc498ab545 --- /dev/null +++ b/packages/router-is-server/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig, mergeConfig } from 'vite' +import { tanstackViteConfig } from '@tanstack/config/vite' + +const config = defineConfig({ + plugins: [], +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: ['./src/server.ts', './src/client.ts', './src/development.ts'], + srcDir: './src', + cjs: true, + }), +) diff --git a/packages/router-ssr-query-core/src/index.ts b/packages/router-ssr-query-core/src/index.ts index 736503fdee8..768fbaab6de 100644 --- a/packages/router-ssr-query-core/src/index.ts +++ b/packages/router-ssr-query-core/src/index.ts @@ -2,7 +2,7 @@ import { dehydrate as queryDehydrate, hydrate as queryHydrate, } from '@tanstack/query-core' -import { isRedirect } from '@tanstack/router-core' +import { isRedirect, isServer } from '@tanstack/router-core' import type { AnyRouter } from '@tanstack/router-core' import type { QueryClient, @@ -35,7 +35,7 @@ export function setupCoreRouterSsrQueryIntegration({ const ogHydrate = router.options.hydrate const ogDehydrate = router.options.dehydrate - if (router.isServer) { + if (isServer ?? router.isServer) { const sentQueries = new Set() const queryStream = createPushableStream() let unsubscribe: (() => void) | undefined = undefined diff --git a/packages/solid-router/src/Asset.tsx b/packages/solid-router/src/Asset.tsx index 4ffe4710c31..13d30918e00 100644 --- a/packages/solid-router/src/Asset.tsx +++ b/packages/solid-router/src/Asset.tsx @@ -1,5 +1,6 @@ import { Link, Meta, Style, Title } from '@solidjs/meta' import { onCleanup, onMount } from 'solid-js' +import { isServer } from '@tanstack/router-core' import { useRouter } from './useRouter' import type { RouterManagedTag } from '@tanstack/router-core' import type { JSX } from 'solid-js' @@ -123,7 +124,7 @@ function Script({ } }) - if (!router.isServer) { + if (!(isServer ?? router.isServer)) { // render an empty script on the client just to avoid hydration errors return null } diff --git a/packages/solid-router/src/Match.tsx b/packages/solid-router/src/Match.tsx index 0e6afc46715..3b2660c4c26 100644 --- a/packages/solid-router/src/Match.tsx +++ b/packages/solid-router/src/Match.tsx @@ -6,6 +6,7 @@ import { getLocationChangeInfo, isNotFound, isRedirect, + isServer, rootRouteId, } from '@tanstack/router-core' import { Dynamic } from 'solid-js/web' @@ -93,7 +94,7 @@ export const Match = (props: { matchId: string }) => { component={ResolvedSuspenseBoundary()} fallback={ // Don't show fallback on server when using no-ssr mode to avoid hydration mismatch - router.isServer || resolvedNoSsr ? undefined : ( + (isServer ?? router.isServer) || resolvedNoSsr ? undefined : ( ) } @@ -129,7 +130,7 @@ export const Match = (props: { matchId: string }) => { } > @@ -271,7 +272,7 @@ export const MatchInner = (props: { matchId: string }): any => { const routerMatch = router.getMatch(match().id) if (routerMatch && !routerMatch._nonReactive.minPendingPromise) { // Create a promise that will resolve after the minPendingMs - if (!router.isServer) { + if (!(isServer ?? router.isServer)) { const minPendingPromise = createControlledPromise() routerMatch._nonReactive.minPendingPromise = minPendingPromise @@ -332,7 +333,7 @@ export const MatchInner = (props: { matchId: string }): any => { {(_) => { - if (router.isServer) { + if (isServer ?? router.isServer) { const RouteErrorComponent = (route().options.errorComponent ?? router.options.defaultErrorComponent) || diff --git a/packages/solid-router/src/Matches.tsx b/packages/solid-router/src/Matches.tsx index efef3252eac..8848421082c 100644 --- a/packages/solid-router/src/Matches.tsx +++ b/packages/solid-router/src/Matches.tsx @@ -1,6 +1,6 @@ import * as Solid from 'solid-js' import warning from 'tiny-warning' -import { rootRouteId } from '@tanstack/router-core' +import { isServer, rootRouteId } from '@tanstack/router-core' import { CatchBoundary, ErrorComponent } from './CatchBoundary' import { useRouterState } from './useRouterState' import { useRouter } from './useRouter' @@ -41,7 +41,8 @@ export function Matches() { const router = useRouter() const ResolvedSuspense = - router.isServer || (typeof document !== 'undefined' && router.ssr) + (isServer ?? router.isServer) || + (typeof document !== 'undefined' && router.ssr) ? SafeFragment : Solid.Suspense diff --git a/packages/solid-router/src/ScriptOnce.tsx b/packages/solid-router/src/ScriptOnce.tsx index 1ad12ccb071..703acf40155 100644 --- a/packages/solid-router/src/ScriptOnce.tsx +++ b/packages/solid-router/src/ScriptOnce.tsx @@ -1,3 +1,4 @@ +import { isServer } from '@tanstack/router-core' import { useRouter } from './useRouter' export function ScriptOnce({ @@ -8,7 +9,7 @@ export function ScriptOnce({ sync?: boolean }) { const router = useRouter() - if (!router.isServer) { + if (!(isServer ?? router.isServer)) { return null } return ( diff --git a/packages/solid-router/src/Transitioner.tsx b/packages/solid-router/src/Transitioner.tsx index c9df0b20537..e6ed2937dea 100644 --- a/packages/solid-router/src/Transitioner.tsx +++ b/packages/solid-router/src/Transitioner.tsx @@ -2,6 +2,7 @@ import * as Solid from 'solid-js' import { getLocationChangeInfo, handleHashScroll, + isServer, trimPathRight, } from '@tanstack/router-core' import { useRouter } from './useRouter' @@ -15,7 +16,7 @@ export function Transitioner() { select: ({ isLoading }) => isLoading, }) - if (router.isServer) { + if (isServer ?? router.isServer) { return null } diff --git a/packages/solid-router/src/scroll-restoration.tsx b/packages/solid-router/src/scroll-restoration.tsx index 8430f8182e0..8a88e9a6711 100644 --- a/packages/solid-router/src/scroll-restoration.tsx +++ b/packages/solid-router/src/scroll-restoration.tsx @@ -1,6 +1,7 @@ import { defaultGetScrollRestorationKey, escapeHtml, + isServer, restoreScroll, storageKey, } from '@tanstack/router-core' @@ -9,7 +10,7 @@ import { ScriptOnce } from './ScriptOnce' export function ScrollRestoration() { const router = useRouter() - if (!router.isScrollRestoring || !router.isServer) { + if (!router.isScrollRestoring || !(isServer ?? router.isServer)) { return null } if (typeof router.options.scrollRestoration === 'function') { diff --git a/packages/solid-router/vite.config.ts b/packages/solid-router/vite.config.ts index 42af3a72b4e..042f35bf4d4 100644 --- a/packages/solid-router/vite.config.ts +++ b/packages/solid-router/vite.config.ts @@ -25,6 +25,13 @@ const config = defineConfig(({ mode }) => { return { plugins: [solid()] as ViteUserConfig['plugins'], + // Add 'development' condition for tests to resolve @tanstack/router-is-server + // to the development export (isServer = undefined) instead of node (isServer = true) + ...(process.env.VITEST && { + resolve: { + conditions: ['development'], + }, + }), test: { name: packageJson.name, dir: './tests', diff --git a/packages/vue-router/src/Asset.tsx b/packages/vue-router/src/Asset.tsx index cf66889d17a..d30ec2cf6da 100644 --- a/packages/vue-router/src/Asset.tsx +++ b/packages/vue-router/src/Asset.tsx @@ -1,4 +1,5 @@ import * as Vue from 'vue' +import { isServer } from '@tanstack/router-core' import { useRouter } from './useRouter' import type { RouterManagedTag } from '@tanstack/router-core' @@ -18,7 +19,7 @@ const Title = Vue.defineComponent({ setup(props) { const router = useRouter() - if (!router.isServer) { + if (!(isServer ?? router.isServer)) { Vue.onMounted(() => { if (props.children) { document.title = props.children @@ -54,7 +55,7 @@ const Script = Vue.defineComponent({ setup(props) { const router = useRouter() - if (!router.isServer) { + if (!(isServer ?? router.isServer)) { Vue.onMounted(() => { const attrs = props.attrs const children = props.children @@ -130,7 +131,7 @@ const Script = Vue.defineComponent({ } return () => { - if (!router.isServer) { + if (!(isServer ?? router.isServer)) { const { src: _src, ...rest } = props.attrs || {} return Vue.h('script', { ...rest, diff --git a/packages/vue-router/src/Match.tsx b/packages/vue-router/src/Match.tsx index c737f446b9c..1933c0b5ee0 100644 --- a/packages/vue-router/src/Match.tsx +++ b/packages/vue-router/src/Match.tsx @@ -6,6 +6,7 @@ import { getLocationChangeInfo, isNotFound, isRedirect, + isServer, rootRouteId, } from '@tanstack/router-core' import { CatchBoundary, ErrorComponent } from './CatchBoundary' @@ -413,7 +414,7 @@ export const MatchInner = Vue.defineComponent({ !routerMatch._nonReactive.minPendingPromise ) { // Create a promise that will resolve after the minPendingMs - if (!router.isServer) { + if (!(isServer ?? router.isServer)) { const minPendingPromise = createControlledPromise() routerMatch._nonReactive.minPendingPromise = minPendingPromise diff --git a/packages/vue-router/src/Matches.tsx b/packages/vue-router/src/Matches.tsx index bb982c72c95..9fefac59a7e 100644 --- a/packages/vue-router/src/Matches.tsx +++ b/packages/vue-router/src/Matches.tsx @@ -1,5 +1,6 @@ import * as Vue from 'vue' import warning from 'tiny-warning' +import { isServer } from '@tanstack/router-core' import { CatchBoundary } from './CatchBoundary' import { useRouterState } from './useRouterState' import { useRouter } from './useRouter' @@ -69,7 +70,8 @@ export const Matches = Vue.defineComponent({ // Do not render a root Suspense during SSR or hydrating from SSR const inner = - router?.isServer || (typeof document !== 'undefined' && router?.ssr) + (isServer ?? router?.isServer ?? false) || + (typeof document !== 'undefined' && router?.ssr) ? Vue.h(MatchesContent) : Vue.h( Vue.Suspense, diff --git a/packages/vue-router/src/ScriptOnce.tsx b/packages/vue-router/src/ScriptOnce.tsx index ecbec969041..1d63983b65a 100644 --- a/packages/vue-router/src/ScriptOnce.tsx +++ b/packages/vue-router/src/ScriptOnce.tsx @@ -1,4 +1,5 @@ import * as Vue from 'vue' +import { isServer } from '@tanstack/router-core' import { useRouter } from './useRouter' export const ScriptOnce = Vue.defineComponent({ @@ -12,7 +13,7 @@ export const ScriptOnce = Vue.defineComponent({ setup(props) { const router = useRouter() - if (router.isServer) { + if (isServer ?? router.isServer) { return () => (