diff --git a/.changeset/quiet-cloudflare-streams.md b/.changeset/quiet-cloudflare-streams.md new file mode 100644 index 00000000000..2378f168b70 --- /dev/null +++ b/.changeset/quiet-cloudflare-streams.md @@ -0,0 +1,6 @@ +--- +'@tanstack/router-core': patch +'@tanstack/start-server-core': patch +--- + +Keep shared SSR stream transforms on Web Streams so Solid Start can use Cloudflare's narrow `nodejs_als` compatibility flag. Remove the unused Node pipeable adapter exports; React keeps its existing Node conversion at the renderer boundary. diff --git a/e2e/solid-start/basic-cloudflare/src/routes/index.tsx b/e2e/solid-start/basic-cloudflare/src/routes/index.tsx index 76e285a2baf..d3f3d7291b6 100644 --- a/e2e/solid-start/basic-cloudflare/src/routes/index.tsx +++ b/e2e/solid-start/basic-cloudflare/src/routes/index.tsx @@ -1,5 +1,6 @@ import { createFileRoute } from '@tanstack/solid-router' import { createServerFn } from '@tanstack/solid-start' +import { getRequestHeader } from '@tanstack/solid-start/server' import { env } from 'cloudflare:workers' export const Route = createFileRoute('/')({ @@ -11,6 +12,7 @@ const getData = createServerFn().handler(() => { return { message: `Running in ${navigator.userAgent}`, myVar: env.MY_VAR, + requestHeader: getRequestHeader('x-solid-start-context'), } }) @@ -22,6 +24,7 @@ function Home() {

Welcome Home!!!

{data().message}

{data().myVar}

+

{data().requestHeader}

) } diff --git a/e2e/solid-start/basic-cloudflare/tests/app.spec.ts b/e2e/solid-start/basic-cloudflare/tests/app.spec.ts index add28cb73ee..6160dabe11a 100644 --- a/e2e/solid-start/basic-cloudflare/tests/app.spec.ts +++ b/e2e/solid-start/basic-cloudflare/tests/app.spec.ts @@ -17,6 +17,16 @@ test('returns the correct value from a Cloudflare binding', async ({ await expect(page.getByTestId('myVar')).toHaveText('Hello from Cloudflare') }) +test('preserves request context with nodejs_als', async ({ page }) => { + await page.setExtraHTTPHeaders({ + 'x-solid-start-context': 'request-context-works', + }) + await page.goto('/') + await expect(page.getByTestId('requestHeader')).toHaveText( + 'request-context-works', + ) +}) + test('prerender with Cloudflare Workers runtime', async ({ page }) => { // Verify the static page was prerendered during build const distDir = join(process.cwd(), 'dist', 'client') diff --git a/e2e/solid-start/basic-cloudflare/worker-configuration.d.ts b/e2e/solid-start/basic-cloudflare/worker-configuration.d.ts index f7a8f163972..be4bba60c37 100644 --- a/e2e/solid-start/basic-cloudflare/worker-configuration.d.ts +++ b/e2e/solid-start/basic-cloudflare/worker-configuration.d.ts @@ -1,18 +1,12 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: b11df627d8b3c51b1bf3230a546b0f20) -// Runtime types generated with workerd@1.20260317.1 2025-09-24 nodejs_compat +// Generated by Wrangler by running `wrangler types` (hash: 24a3fe972e2c4aaa57bb30eaba75ee7a) +// Runtime types generated with workerd@1.20260317.1 2025-09-24 nodejs_als declare namespace Cloudflare { interface Env { MY_VAR: "Hello from Cloudflare"; } } interface Env extends Cloudflare.Env {} -type StringifyValues> = { - [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; -}; -declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} -} // Begin runtime types /*! ***************************************************************************** diff --git a/e2e/solid-start/basic-cloudflare/wrangler.jsonc b/e2e/solid-start/basic-cloudflare/wrangler.jsonc index 833d63d85a6..d218706c38b 100644 --- a/e2e/solid-start/basic-cloudflare/wrangler.jsonc +++ b/e2e/solid-start/basic-cloudflare/wrangler.jsonc @@ -2,7 +2,7 @@ "$schema": "node_modules/wrangler/config-schema.json", "name": "tanstack-start-app", "compatibility_date": "2025-09-24", - "compatibility_flags": ["nodejs_compat"], + "compatibility_flags": ["nodejs_als"], "main": "@tanstack/solid-start/server-entry", "vars": { "MY_VAR": "Hello from Cloudflare", diff --git a/examples/solid/start-basic-cloudflare/worker-configuration.d.ts b/examples/solid/start-basic-cloudflare/worker-configuration.d.ts index f7a8f163972..be4bba60c37 100644 --- a/examples/solid/start-basic-cloudflare/worker-configuration.d.ts +++ b/examples/solid/start-basic-cloudflare/worker-configuration.d.ts @@ -1,18 +1,12 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: b11df627d8b3c51b1bf3230a546b0f20) -// Runtime types generated with workerd@1.20260317.1 2025-09-24 nodejs_compat +// Generated by Wrangler by running `wrangler types` (hash: 24a3fe972e2c4aaa57bb30eaba75ee7a) +// Runtime types generated with workerd@1.20260317.1 2025-09-24 nodejs_als declare namespace Cloudflare { interface Env { MY_VAR: "Hello from Cloudflare"; } } interface Env extends Cloudflare.Env {} -type StringifyValues> = { - [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; -}; -declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} -} // Begin runtime types /*! ***************************************************************************** diff --git a/examples/solid/start-basic-cloudflare/wrangler.jsonc b/examples/solid/start-basic-cloudflare/wrangler.jsonc index 833d63d85a6..d218706c38b 100644 --- a/examples/solid/start-basic-cloudflare/wrangler.jsonc +++ b/examples/solid/start-basic-cloudflare/wrangler.jsonc @@ -2,7 +2,7 @@ "$schema": "node_modules/wrangler/config-schema.json", "name": "tanstack-start-app", "compatibility_date": "2025-09-24", - "compatibility_flags": ["nodejs_compat"], + "compatibility_flags": ["nodejs_als"], "main": "@tanstack/solid-start/server-entry", "vars": { "MY_VAR": "Hello from Cloudflare", diff --git a/packages/react-router/tests/renderRouterToStream.test.tsx b/packages/react-router/tests/renderRouterToStream.test.tsx index cde31d0b6a9..ae19febb3a4 100644 --- a/packages/react-router/tests/renderRouterToStream.test.tsx +++ b/packages/react-router/tests/renderRouterToStream.test.tsx @@ -1,6 +1,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest' import { attachRouterServerSsrUtils } from '@tanstack/router-core/ssr/server' import { createMemoryHistory, createRootRoute, createRouter } from '../src' +import type { PassThrough } from 'node:stream' import type { RouterManagedTag } from '@tanstack/router-core' import type * as RouterSsrServer from '@tanstack/router-core/ssr/server' import type * as ReactDomServer from 'react-dom/server' @@ -742,6 +743,7 @@ describe('renderRouterToStream - renderer selection and pipeable errors', () => let options: | Parameters[1] | undefined + let destination: PassThrough | undefined const abort = vi.fn((reason: unknown) => { options?.onError?.(reason, { componentStack: '' }) }) @@ -749,11 +751,18 @@ describe('renderRouterToStream - renderer selection and pipeable errors', () => (_children, nextOptions) => { options = nextOptions queueMicrotask(() => nextOptions.onShellReady()) - return { abort, pipe: vi.fn() } + return { + abort, + pipe: vi.fn((stream: PassThrough) => { + destination = stream + return stream + }), + } }, ) const router = await buildRouter() + const cleanup = vi.spyOn(router.serverSsr!, 'cleanup') const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const reason = new Error('pipeable-consumer-gone') try { @@ -767,7 +776,9 @@ describe('renderRouterToStream - renderer selection and pipeable errors', () => ) await response.body!.cancel(reason) - expect(abort).toHaveBeenCalledWith(reason) + expect(abort).toHaveBeenCalledExactlyOnceWith(reason) + await vi.waitFor(() => expect(destination?.destroyed).toBe(true)) + expect(cleanup).toHaveBeenCalledOnce() expect(errorSpy).not.toHaveBeenCalled() } finally { errorSpy.mockRestore() diff --git a/packages/router-core/src/ssr/server.ts b/packages/router-core/src/ssr/server.ts index 84c34976c98..c0d2fd8be93 100644 --- a/packages/router-core/src/ssr/server.ts +++ b/packages/router-core/src/ssr/server.ts @@ -17,7 +17,6 @@ export type { SsrResponse, } from './handlerCallback' export { - transformPipeableStreamWithRouter, transformHtmlStringWithRouter, transformReadableStreamWithRouter, } from './transformStreamWithRouter' diff --git a/packages/router-core/src/ssr/transformStreamWithRouter.ts b/packages/router-core/src/ssr/transformStreamWithRouter.ts index d253034073b..3c82bb6b47c 100644 --- a/packages/router-core/src/ssr/transformStreamWithRouter.ts +++ b/packages/router-core/src/ssr/transformStreamWithRouter.ts @@ -1,4 +1,3 @@ -import { Readable } from 'node:stream' import { DOCUMENT_CLOSE_ANCHOR_INDEX, DOCUMENT_CLOSE_BYTES, @@ -49,20 +48,6 @@ type AppStream = | ReadableStream | NodeReadableStream -export function transformPipeableStreamWithRouter( - router: AnyRouter, - routerStream: Readable, - opts?: TransformStreamWithRouterOptions, -) { - return Readable.fromWeb( - transformReadableStreamWithRouter( - router, - Readable.toWeb(routerStream) as AppStream, - opts, - ) as NodeReadableStream, - ) -} - export async function transformHtmlStringWithRouter( router: AnyRouter, html: string, diff --git a/packages/router-core/tests/transformStreamWithRouter.test.ts b/packages/router-core/tests/transformStreamWithRouter.test.ts index e5ce1d8d50d..056ff71f908 100644 --- a/packages/router-core/tests/transformStreamWithRouter.test.ts +++ b/packages/router-core/tests/transformStreamWithRouter.test.ts @@ -21,7 +21,6 @@ import { import { attachRouterServerSsrUtils } from '../src/ssr/ssr-server' import { transformHtmlStringWithRouter, - transformPipeableStreamWithRouter, transformReadableStreamWithRouter, } from '../src/ssr/transformStreamWithRouter' import { @@ -29,9 +28,27 @@ import { SCRIPT_CLOSE, } from '../src/ssr/htmlBoundaryScanner' import { createTestRouter } from './routerTestUtils' +import type { TransformStreamWithRouterOptions } from '../src/ssr/transformStreamWithRouter' +import type { AnyRouter } from '../src/router' +import type { ReadableStream as NodeReadableStream } from 'node:stream/web' import type { RouterManagedTag } from '../src/manifest' import type { HydrationScriptOutput } from '../src/ssr/hydrationScripts' +// Node interoperability belongs at the caller boundary, not in the shared SSR module. +function transformPipeableStreamWithRouter( + router: AnyRouter, + stream: Readable, + opts?: TransformStreamWithRouterOptions, +) { + return Readable.fromWeb( + transformReadableStreamWithRouter( + router, + Readable.toWeb(stream) as ReadableStream, + opts, + ) as NodeReadableStream, + ) +} + const decoder = new TextDecoder() const DOCUMENT_CLOSE = decoder.decode(DOCUMENT_CLOSE_BYTES) const HYDRATION_SCRIPT_BOUNDARY_SUFFIX = decoder.decode( diff --git a/packages/start-server-core/src/index.tsx b/packages/start-server-core/src/index.tsx index 92937ff0392..3dbf00cbf1d 100644 --- a/packages/start-server-core/src/index.tsx +++ b/packages/start-server-core/src/index.tsx @@ -21,7 +21,6 @@ export { createRequestHandler, defineHandlerCallback, transformReadableStreamWithRouter, - transformPipeableStreamWithRouter, } from '@tanstack/router-core/ssr/server' export type { HandlerCallback } from '@tanstack/router-core/ssr/server'