From 108d15fe50548b2e3bb9a8c98eedcb4cfb93dbc1 Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 26 Sep 2026 18:12:34 +0200
Subject: [PATCH] test: port compiler correctness coverage from Yuku migration
---
.../router-generator/tests/transform.test.ts | 13 ++
.../tests/code-splitter-jsx.test.ts | 66 +++++++
.../tests/code-splitter-runtime.test.ts | 161 ++++++++++++++++++
.../react/1-default/shared-runtime.tsx | 23 +++
.../1-default/shared-runtime@component.tsx | 23 +++
.../shared-runtime@errorComponent.tsx | 11 ++
.../shared-runtime@notFoundComponent.tsx | 11 ++
.../react/1-default/shared-runtime@shared.tsx | 28 +++
.../shared-runtime.tsx | 22 +++
...--notFoundComponent---pendingComponent.tsx | 23 +++
.../shared-runtime@loader.tsx | 16 ++
.../shared-runtime@shared.tsx | 28 +++
.../shared-runtime.tsx | 22 +++
...--notFoundComponent---pendingComponent.tsx | 28 +++
.../shared-runtime@errorComponent.tsx | 11 ++
.../shared-runtime@shared.tsx | 28 +++
.../test-files/react/shared-runtime.tsx | 50 ++++++
.../tests/router-plugin-context.test.ts | 138 +++++++++++++++
.../start-plugin-core/tests/compiler.test.ts | 124 ++++++++++++++
.../tests/hydrate-when-transform.test.ts | 88 ++++++++++
.../tests/hydrateWhen/hydrateWhen.test.ts | 58 +++++++
.../importProtection/analysis.exports.test.ts | 14 ++
.../importProtection/analysis.usage.test.ts | 37 +++-
23 files changed, 1022 insertions(+), 1 deletion(-)
create mode 100644 packages/router-plugin/tests/code-splitter-jsx.test.ts
create mode 100644 packages/router-plugin/tests/code-splitter-runtime.test.ts
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@component.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@errorComponent.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@notFoundComponent.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@shared.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime@component---errorComponent---notFoundComponent---pendingComponent.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime@loader.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime@shared.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime@component---loader---notFoundComponent---pendingComponent.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime@errorComponent.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime@shared.tsx
create mode 100644 packages/router-plugin/tests/code-splitter/test-files/react/shared-runtime.tsx
diff --git a/packages/router-generator/tests/transform.test.ts b/packages/router-generator/tests/transform.test.ts
index 8a53f3278e8..f026ecc30e6 100644
--- a/packages/router-generator/tests/transform.test.ts
+++ b/packages/router-generator/tests/transform.test.ts
@@ -14,6 +14,19 @@ function makeNode(
}
describe('transform', () => {
+ it('updates parenthesized route calls without changing their surrounding source', () => {
+ const result = transform({
+ source:
+ "import { createFileRoute } from '@tanstack/react-router'; export const Route = ((createFileRoute('/old'))({}));",
+ ctx: { target: 'react', routeId: '/new', lazy: false },
+ node: makeNode(),
+ })
+ expect(result.result).toBe('modified')
+ if (result.result === 'modified') {
+ expect(result.output).toContain("((createFileRoute('/new'))({}))")
+ }
+ })
+
it('does not treat root route exports as missing Route exports', async () => {
const result = await transform({
source: [
diff --git a/packages/router-plugin/tests/code-splitter-jsx.test.ts b/packages/router-plugin/tests/code-splitter-jsx.test.ts
new file mode 100644
index 00000000000..0718a5da2a0
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter-jsx.test.ts
@@ -0,0 +1,66 @@
+import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
+import path from 'node:path'
+import { createServer } from 'vite'
+import { expect, it } from 'vitest'
+import { tanstackRouter } from '../src/vite'
+
+it.each(['Widget', '_Widget', '$Widget'])(
+ 'preserves the shared JSX component %s across separate loader and component chunks',
+ async (name) => {
+ // Keep the temporary app inside the package so runtime imports resolve.
+ const root = await mkdtemp(path.join(__dirname, '.jsx-reference-'))
+ let server: Awaited> | undefined
+ try {
+ await mkdir(path.join(root, 'routes'))
+ await writeFile(
+ path.join(root, 'routes/__root.tsx'),
+ `import { createRootRoute } from '@tanstack/react-router'
+export const Route = createRootRoute({})`,
+ )
+ await writeFile(
+ path.join(root, 'routes/widget.tsx'),
+ `import React from 'react'
+import { createFileRoute } from '@tanstack/react-router'
+const ${name} = () => 'hello'
+export const Route = createFileRoute('/widget')({
+ loader: () => ${name},
+ component: () => <${name} />,
+})`,
+ )
+ server = await createServer({
+ root,
+ configFile: false,
+ logLevel: 'silent',
+ server: { watch: null },
+ plugins: [
+ tanstackRouter({
+ target: 'react',
+ routesDirectory: './routes',
+ generatedRouteTree: './routeTree.gen.ts',
+ autoCodeSplitting: true,
+ codeSplittingOptions: {
+ addHmr: false,
+ defaultBehavior: [['loader'], ['component']],
+ },
+ }),
+ ],
+ })
+ // The physical module establishes shared ownership before chunks load.
+ await server.ssrLoadModule('/routes/widget.tsx')
+ const componentModule = await server.ssrLoadModule(
+ '/routes/widget.tsx?tsr-split=component',
+ )
+ const loaderModule = await server.ssrLoadModule(
+ '/routes/widget.tsx?tsr-split=loader',
+ )
+ const element = componentModule.component()
+ expect(typeof element.type).toBe('function')
+ expect(element.type()).toBe('hello')
+ expect(loaderModule.loader()()).toBe('hello')
+ } finally {
+ await server?.close()
+ await rm(root, { recursive: true, force: true })
+ }
+ },
+ 30_000,
+)
diff --git a/packages/router-plugin/tests/code-splitter-runtime.test.ts b/packages/router-plugin/tests/code-splitter-runtime.test.ts
new file mode 100644
index 00000000000..0eb9b52ea54
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter-runtime.test.ts
@@ -0,0 +1,161 @@
+import { execFile } from 'node:child_process'
+import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
+import path from 'node:path'
+import { promisify } from 'node:util'
+import { pathToFileURL } from 'node:url'
+import { build } from 'vite'
+import { expect, it } from 'vitest'
+import { tanstackRouter } from '../src/vite'
+import type { CodeSplitGroupings } from '../src/core/constants'
+
+const runNode = promisify(execFile)
+const fixture = path.resolve(
+ __dirname,
+ 'code-splitter/test-files/react/shared-runtime.tsx',
+)
+
+const groupings: Array<{ name: string; groupings: CodeSplitGroupings }> = [
+ { name: 'eager loader', groupings: [['component']] },
+ { name: 'separate lazy loader', groupings: [['component'], ['loader']] },
+ { name: 'combined lazy loader', groupings: [['component', 'loader']] },
+]
+
+it.each(groupings)(
+ 'preserves executable route contracts with $name',
+ assertRuntimeContracts,
+ 30_000,
+)
+
+// Shared extraction currently duplicates named default function declarations and
+// drops TypeScript enum/namespace dependencies. Keep their intended contracts.
+it.fails.each(['named-default', 'enum', 'namespace'] as const)(
+ 'preserves executable route contracts with shared %s declarations',
+ async (syntax) => {
+ await assertRuntimeContracts({
+ groupings: [['component'], ['loader']],
+ syntax,
+ })
+ },
+ 30_000,
+)
+
+async function assertRuntimeContracts({
+ groupings,
+ syntax,
+}: {
+ groupings: CodeSplitGroupings
+ syntax?: 'named-default' | 'enum' | 'namespace'
+}) {
+ // Keep the temporary app inside the package so real runtime imports resolve.
+ const root = await mkdtemp(path.join(__dirname, '.runtime-contract-'))
+ try {
+ await mkdir(path.join(root, 'routes'))
+ let source = await readFile(fixture, 'utf8')
+ if (syntax === 'named-default') {
+ source = source
+ .replace(
+ 'function createSeed()',
+ 'export default function createSeed()',
+ )
+ .replace('export default createSeed', '')
+ } else if (syntax === 'enum') {
+ source = source.replace(
+ 'const Count = { Initial: 0 }',
+ 'enum Count { Initial = 0 }',
+ )
+ } else if (syntax === 'namespace') {
+ source = source.replace(
+ "const Labels = { component: 'count' }",
+ "namespace Labels { export const component = 'count' }",
+ )
+ }
+ await writeFile(path.join(root, 'routes/shared-runtime.tsx'), source)
+ await writeFile(
+ path.join(root, 'routes/__root.tsx'),
+ `import { createRootRoute } from '@tanstack/react-router'
+export const Route = createRootRoute({})`,
+ )
+ await writeFile(
+ path.join(root, 'entry.ts'),
+ `import { createElement } from 'react'
+import { renderToString } from 'react-dom/server'
+import { Route } from './routes/shared-runtime'
+export { Route, firstState, secondState, 'odd-name' } from './routes/shared-runtime'
+export function render() {
+ return renderToString(createElement(Route.options.component))
+}`,
+ )
+
+ await build({
+ root,
+ configFile: false,
+ logLevel: 'silent',
+ plugins: [
+ tanstackRouter({
+ target: 'react',
+ routesDirectory: './routes',
+ generatedRouteTree: './routeTree.gen.ts',
+ autoCodeSplitting: true,
+ codeSplittingOptions: {
+ addHmr: false,
+ defaultBehavior: groupings,
+ },
+ }),
+ ],
+ build: {
+ ssr: path.join(root, 'entry.ts'),
+ outDir: 'dist',
+ minify: false,
+ sourcemap: true,
+ rollupOptions: {
+ output: {
+ entryFileNames: 'entry.mjs',
+ chunkFileNames: '[name].mjs',
+ },
+ },
+ },
+ })
+
+ const throwLine =
+ source.split('\n').findIndex((line) => line.includes('throw new Error')) +
+ 1
+ const entryUrl = pathToFileURL(path.join(root, 'dist/entry.mjs')).href
+ const combined = groupings.some(
+ (group) => group.includes('component') && group.includes('loader'),
+ )
+ const { stdout } = await runNode(process.execPath, [
+ '--enable-source-maps',
+ '--input-type=module',
+ '--eval',
+ `import assert from 'node:assert/strict'
+const messages = []
+console.log = (message) => messages.push(message)
+const { Route, render, firstState, secondState, 'odd-name': oddState } = await import(${JSON.stringify(entryUrl)})
+assert.deepEqual(messages, ['shared-runtime:seed', 'shared-runtime:state', 'shared-runtime:reference'])
+const state = Route.options.beforeLoad({})
+assert.strictEqual(firstState, state)
+assert.strictEqual(secondState, state)
+assert.strictEqual(oddState, state)
+assert.equal(state.count, 0)
+assert.strictEqual(await Route.options.loader({}), state)
+assert.equal(state.count, 1)
+assert.deepEqual(messages, ${JSON.stringify(combined ? ['shared-runtime:seed', 'shared-runtime:state', 'shared-runtime:reference', 'shared-runtime:component'] : ['shared-runtime:seed', 'shared-runtime:state', 'shared-runtime:reference'])})
+await Route.options.component.preload()
+assert.equal(render(), 'count:1')
+assert.strictEqual(await Route.options.loader({}), state)
+assert.equal(render(), 'count:2')
+await Route.options.component.preload()
+assert.deepEqual(messages, ['shared-runtime:seed', 'shared-runtime:state', 'shared-runtime:reference', 'shared-runtime:component'])
+await Route.options.loader({})
+assert.throws(render, (error) => {
+ assert.equal(error.message, 'route source-map contract')
+ assert.match(error.stack, /shared-runtime\\.tsx(?:\\?[^\\n:]+)?:${throwLine}:/)
+ return true
+})
+process.stdout.write('route contracts passed')`,
+ ])
+ expect(stdout).toBe('route contracts passed')
+ } finally {
+ await rm(root, { recursive: true, force: true })
+ }
+}
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime.tsx
new file mode 100644
index 00000000000..c230b4d37f8
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime.tsx
@@ -0,0 +1,23 @@
+import { state, createState, createSeed, increment } from "shared-runtime.tsx?tsr-shared=1";
+const $$splitComponentImporter = () => import('shared-runtime.tsx?tsr-split=component');
+import { lazyRouteComponent } from '@tanstack/react-router';
+import { createFileRoute } from '@tanstack/react-router';
+console.log('shared-runtime:reference');
+export const Route = createFileRoute('/shared-runtime')({
+ beforeLoad: () => state,
+ loader: () => {
+ increment();
+ return state;
+ },
+ component: lazyRouteComponent($$splitComponentImporter, 'component')
+});
+function createState(initialState: {
+ count: number;
+}): {
+ state: {
+ count: number;
+ };
+ increment: () => number;
+};
+export { state as firstState, state as secondState, state as 'odd-name' };
+export default createSeed;
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@component.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@component.tsx
new file mode 100644
index 00000000000..fbccf6c90bc
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@component.tsx
@@ -0,0 +1,23 @@
+import { state, createState, createSeed, Labels } from "shared-runtime.tsx?tsr-shared=1";
+const label = createComponentLabel();
+function createState(initialState: {
+ count: number;
+}): {
+ state: {
+ count: number;
+ };
+ increment: () => number;
+};
+function createComponentLabel() {
+ console.log('shared-runtime:component');
+ return Labels.component;
+}
+export { state as firstState, state as secondState, state as 'odd-name' };
+export default createSeed;
+const SplitComponent = () => {
+ if (state.count === 3) {
+ throw new Error('route source-map contract');
+ }
+ return `${label}:${state.count}`;
+};
+export { SplitComponent as component };
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@errorComponent.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@errorComponent.tsx
new file mode 100644
index 00000000000..d890a562c94
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@errorComponent.tsx
@@ -0,0 +1,11 @@
+import { state, createState, createSeed } from "shared-runtime.tsx?tsr-shared=1";
+function createState(initialState: {
+ count: number;
+}): {
+ state: {
+ count: number;
+ };
+ increment: () => number;
+};
+export { state as firstState, state as secondState, state as 'odd-name' };
+export default createSeed;
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@notFoundComponent.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@notFoundComponent.tsx
new file mode 100644
index 00000000000..d890a562c94
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@notFoundComponent.tsx
@@ -0,0 +1,11 @@
+import { state, createState, createSeed } from "shared-runtime.tsx?tsr-shared=1";
+function createState(initialState: {
+ count: number;
+}): {
+ state: {
+ count: number;
+ };
+ increment: () => number;
+};
+export { state as firstState, state as secondState, state as 'odd-name' };
+export default createSeed;
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@shared.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@shared.tsx
new file mode 100644
index 00000000000..54239b6cc59
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/shared-runtime@shared.tsx
@@ -0,0 +1,28 @@
+const Count = {
+ Initial: 0
+};
+const Labels = {
+ component: 'count'
+};
+const seed = createSeed();
+const {
+ state,
+ increment
+} = createState(seed);
+function createSeed() {
+ console.log('shared-runtime:seed');
+ return {
+ count: Count.Initial,
+ label: Labels.component
+ };
+}
+function createState(initialState: {
+ count: number;
+}) {
+ console.log('shared-runtime:state');
+ return {
+ state: initialState,
+ increment: () => initialState.count++
+ };
+}
+export { Count, createSeed, createState, increment, Labels, seed, state };
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime.tsx
new file mode 100644
index 00000000000..bceb12d39a4
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime.tsx
@@ -0,0 +1,22 @@
+import { state, createState, createSeed } from "shared-runtime.tsx?tsr-shared=1";
+const $$splitComponentImporter = () => import('shared-runtime.tsx?tsr-split=component---errorComponent---notFoundComponent---pendingComponent');
+import { lazyRouteComponent } from '@tanstack/react-router';
+const $$splitLoaderImporter = () => import('shared-runtime.tsx?tsr-split=loader');
+import { lazyFn } from '@tanstack/react-router';
+import { createFileRoute } from '@tanstack/react-router';
+console.log('shared-runtime:reference');
+export const Route = createFileRoute('/shared-runtime')({
+ beforeLoad: () => state,
+ loader: lazyFn($$splitLoaderImporter, 'loader'),
+ component: lazyRouteComponent($$splitComponentImporter, 'component')
+});
+function createState(initialState: {
+ count: number;
+}): {
+ state: {
+ count: number;
+ };
+ increment: () => number;
+};
+export { state as firstState, state as secondState, state as 'odd-name' };
+export default createSeed;
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime@component---errorComponent---notFoundComponent---pendingComponent.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime@component---errorComponent---notFoundComponent---pendingComponent.tsx
new file mode 100644
index 00000000000..fbccf6c90bc
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime@component---errorComponent---notFoundComponent---pendingComponent.tsx
@@ -0,0 +1,23 @@
+import { state, createState, createSeed, Labels } from "shared-runtime.tsx?tsr-shared=1";
+const label = createComponentLabel();
+function createState(initialState: {
+ count: number;
+}): {
+ state: {
+ count: number;
+ };
+ increment: () => number;
+};
+function createComponentLabel() {
+ console.log('shared-runtime:component');
+ return Labels.component;
+}
+export { state as firstState, state as secondState, state as 'odd-name' };
+export default createSeed;
+const SplitComponent = () => {
+ if (state.count === 3) {
+ throw new Error('route source-map contract');
+ }
+ return `${label}:${state.count}`;
+};
+export { SplitComponent as component };
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime@loader.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime@loader.tsx
new file mode 100644
index 00000000000..6c797c0226f
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime@loader.tsx
@@ -0,0 +1,16 @@
+import { state, createState, createSeed, increment } from "shared-runtime.tsx?tsr-shared=1";
+function createState(initialState: {
+ count: number;
+}): {
+ state: {
+ count: number;
+ };
+ increment: () => number;
+};
+export { state as firstState, state as secondState, state as 'odd-name' };
+export default createSeed;
+const SplitLoader = () => {
+ increment();
+ return state;
+};
+export { SplitLoader as loader };
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime@shared.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime@shared.tsx
new file mode 100644
index 00000000000..54239b6cc59
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/shared-runtime@shared.tsx
@@ -0,0 +1,28 @@
+const Count = {
+ Initial: 0
+};
+const Labels = {
+ component: 'count'
+};
+const seed = createSeed();
+const {
+ state,
+ increment
+} = createState(seed);
+function createSeed() {
+ console.log('shared-runtime:seed');
+ return {
+ count: Count.Initial,
+ label: Labels.component
+ };
+}
+function createState(initialState: {
+ count: number;
+}) {
+ console.log('shared-runtime:state');
+ return {
+ state: initialState,
+ increment: () => initialState.count++
+ };
+}
+export { Count, createSeed, createState, increment, Labels, seed, state };
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime.tsx
new file mode 100644
index 00000000000..d88eff9d9b9
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime.tsx
@@ -0,0 +1,22 @@
+import { state, createState, createSeed } from "shared-runtime.tsx?tsr-shared=1";
+const $$splitComponentImporter = () => import('shared-runtime.tsx?tsr-split=component---loader---notFoundComponent---pendingComponent');
+import { lazyRouteComponent } from '@tanstack/react-router';
+const $$splitLoaderImporter = () => import('shared-runtime.tsx?tsr-split=component---loader---notFoundComponent---pendingComponent');
+import { lazyFn } from '@tanstack/react-router';
+import { createFileRoute } from '@tanstack/react-router';
+console.log('shared-runtime:reference');
+export const Route = createFileRoute('/shared-runtime')({
+ beforeLoad: () => state,
+ loader: lazyFn($$splitLoaderImporter, 'loader'),
+ component: lazyRouteComponent($$splitComponentImporter, 'component')
+});
+function createState(initialState: {
+ count: number;
+}): {
+ state: {
+ count: number;
+ };
+ increment: () => number;
+};
+export { state as firstState, state as secondState, state as 'odd-name' };
+export default createSeed;
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime@component---loader---notFoundComponent---pendingComponent.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime@component---loader---notFoundComponent---pendingComponent.tsx
new file mode 100644
index 00000000000..0af87c21c51
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime@component---loader---notFoundComponent---pendingComponent.tsx
@@ -0,0 +1,28 @@
+import { state, createState, createSeed, Labels, increment } from "shared-runtime.tsx?tsr-shared=1";
+const label = createComponentLabel();
+function createState(initialState: {
+ count: number;
+}): {
+ state: {
+ count: number;
+ };
+ increment: () => number;
+};
+function createComponentLabel() {
+ console.log('shared-runtime:component');
+ return Labels.component;
+}
+export { state as firstState, state as secondState, state as 'odd-name' };
+export default createSeed;
+const SplitLoader = () => {
+ increment();
+ return state;
+};
+export { SplitLoader as loader };
+const SplitComponent = () => {
+ if (state.count === 3) {
+ throw new Error('route source-map contract');
+ }
+ return `${label}:${state.count}`;
+};
+export { SplitComponent as component };
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime@errorComponent.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime@errorComponent.tsx
new file mode 100644
index 00000000000..d890a562c94
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime@errorComponent.tsx
@@ -0,0 +1,11 @@
+import { state, createState, createSeed } from "shared-runtime.tsx?tsr-shared=1";
+function createState(initialState: {
+ count: number;
+}): {
+ state: {
+ count: number;
+ };
+ increment: () => number;
+};
+export { state as firstState, state as secondState, state as 'odd-name' };
+export default createSeed;
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime@shared.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime@shared.tsx
new file mode 100644
index 00000000000..54239b6cc59
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/shared-runtime@shared.tsx
@@ -0,0 +1,28 @@
+const Count = {
+ Initial: 0
+};
+const Labels = {
+ component: 'count'
+};
+const seed = createSeed();
+const {
+ state,
+ increment
+} = createState(seed);
+function createSeed() {
+ console.log('shared-runtime:seed');
+ return {
+ count: Count.Initial,
+ label: Labels.component
+ };
+}
+function createState(initialState: {
+ count: number;
+}) {
+ console.log('shared-runtime:state');
+ return {
+ state: initialState,
+ increment: () => initialState.count++
+ };
+}
+export { Count, createSeed, createState, increment, Labels, seed, state };
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/test-files/react/shared-runtime.tsx b/packages/router-plugin/tests/code-splitter/test-files/react/shared-runtime.tsx
new file mode 100644
index 00000000000..f9fd708a0ba
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/test-files/react/shared-runtime.tsx
@@ -0,0 +1,50 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+const Count = { Initial: 0 }
+
+const Labels = { component: 'count' }
+
+const seed = createSeed()
+const { state, increment } = createState(seed)
+const label = createComponentLabel()
+console.log('shared-runtime:reference')
+
+export const Route = createFileRoute('/shared-runtime')({
+ beforeLoad: () => state,
+ loader: () => {
+ increment()
+ return state
+ },
+ component: () => {
+ if (state.count === 3) {
+ throw new Error('route source-map contract')
+ }
+ return `${label}:${state.count}`
+ },
+})
+
+function createSeed() {
+ console.log('shared-runtime:seed')
+ return { count: Count.Initial, label: Labels.component }
+}
+
+function createState(initialState: { count: number }): {
+ state: { count: number }
+ increment: () => number
+}
+function createState(initialState: { count: number }) {
+ console.log('shared-runtime:state')
+ return {
+ state: initialState,
+ increment: () => initialState.count++,
+ }
+}
+
+function createComponentLabel() {
+ console.log('shared-runtime:component')
+ return Labels.component
+}
+
+export { state as firstState, state as secondState, state as 'odd-name' }
+
+export default createSeed
diff --git a/packages/router-plugin/tests/router-plugin-context.test.ts b/packages/router-plugin/tests/router-plugin-context.test.ts
index b8e317c8ecb..d3fb0563e60 100644
--- a/packages/router-plugin/tests/router-plugin-context.test.ts
+++ b/packages/router-plugin/tests/router-plugin-context.test.ts
@@ -90,6 +90,144 @@ function countProgramHotDeclarations(code: string) {
}
describe('router plugin context', () => {
+ it('splits parenthesized route factories, option values, and grouping arrays', async () => {
+ const routeFile = normalizePath(
+ path.join(process.cwd(), 'src/routes/wrapped.tsx'),
+ )
+ const context = createRouterPluginContext()
+ context.routesByFile.set(routeFile, { routeId: '/wrapped' })
+ const plugins = createRouterCodeSplitterPlugin(
+ {
+ target: 'react',
+ autoCodeSplitting: true,
+ codeSplittingOptions: { addHmr: false },
+ },
+ context,
+ )
+ await configurePlugin(getReferencePlugin(plugins), 'build')
+ const code = `import { createFileRoute } from '@tanstack/react-router'
+const Component = () => 'wrapped component'
+const loader = () => 'wrapped loader'
+export const Route = (createFileRoute('/wrapped'))({
+ component: (Component), loader: (loader),
+ codeSplitGroupings: (([(['component']), (['loader'])])),
+})`
+ const reference = getCode(
+ await transformReferenceRoute(
+ getReferencePlugin(plugins),
+ code,
+ routeFile,
+ ),
+ )
+ expect(reference).toContain('lazyRouteComponent')
+ expect(reference).toContain('lazyFn')
+ const virtual = getCode(
+ await transformReferenceRoute(
+ getCodeSplitterPlugin(plugins, virtualPluginName),
+ code,
+ `${routeFile}?tsr-split=component`,
+ ),
+ )
+ expect(virtual).toContain('wrapped component')
+ expect(virtual).not.toContain('wrapped loader')
+ })
+
+ it('refreshes chunk ownership after source changes and isolates split settings', async () => {
+ const routeFile = normalizePath(
+ path.join(process.cwd(), 'src/routes/changing.tsx'),
+ )
+ const source = (name: string, value: string) => `
+import { createFileRoute } from '@tanstack/react-router'
+const ${name} = { value: ${JSON.stringify(value)} }
+export const Route = createFileRoute('/changing')({
+ loader: () => ${name},
+ component: () => ${name}.value,
+})`
+ const setup = async (combined: boolean) => {
+ const context = createRouterPluginContext()
+ context.routesByFile.set(routeFile, { routeId: '/changing' })
+ const plugins = createRouterCodeSplitterPlugin(
+ {
+ target: 'react',
+ autoCodeSplitting: true,
+ codeSplittingOptions: {
+ addHmr: false,
+ defaultBehavior: combined
+ ? [['component', 'loader']]
+ : [['component'], ['loader']],
+ },
+ },
+ context,
+ )
+ await configurePlugin(getReferencePlugin(plugins), 'build')
+ return async (code: string) => {
+ const reference = getCode(
+ await transformReferenceRoute(
+ getReferencePlugin(plugins),
+ code,
+ routeFile,
+ ),
+ )!
+ const virtual = getCode(
+ await transformReferenceRoute(
+ getCodeSplitterPlugin(plugins, virtualPluginName),
+ code,
+ `${routeFile}?tsr-split=${combined ? 'component---loader' : 'component'}`,
+ ),
+ )!
+ const shared = getCode(
+ await transformReferenceRoute(
+ getCodeSplitterPlugin(
+ plugins,
+ 'tanstack-router:code-splitter:compile-shared-file',
+ ),
+ code,
+ `${routeFile}?tsr-shared=1`,
+ ),
+ )!
+ return { reference, virtual, shared }
+ }
+ }
+ const separate = await setup(false)
+ const combined = await setup(true)
+ const first = await separate(source('firstState', 'first version'))
+ expect(first.reference).toContain('tsr-split=component')
+ const virtualImports = parseAst({
+ code: first.virtual,
+ }).program.body.filter((statement) => t.isImportDeclaration(statement))
+ expect(virtualImports).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ source: expect.objectContaining({
+ value: expect.stringContaining('tsr-shared'),
+ }),
+ specifiers: expect.arrayContaining([
+ expect.objectContaining({
+ type: 'ImportSpecifier',
+ imported: expect.objectContaining({ name: 'firstState' }),
+ local: expect.objectContaining({ name: 'firstState' }),
+ }),
+ ]),
+ }),
+ ]),
+ )
+ expect(first.shared).toContain('first version')
+
+ const second = await separate(source('secondState', 'second version'))
+ for (const output of Object.values(second)) {
+ expect(output).not.toContain('firstState')
+ }
+ expect(second.virtual).toContain('secondState')
+ expect(second.shared).toContain('second version')
+ const together = await combined(source('secondState', 'second version'))
+ expect(together.reference).not.toContain('tsr-shared')
+ expect(together.virtual).toContain('second version')
+ expect(together.virtual).not.toContain('tsr-shared')
+ expect(await separate(source('secondState', 'second version'))).toEqual(
+ second,
+ )
+ })
+
it.each([
{ mode: 'production', production: true },
{ mode: 'development with addHmr disabled', production: false },
diff --git a/packages/start-plugin-core/tests/compiler.test.ts b/packages/start-plugin-core/tests/compiler.test.ts
index 94780d140b2..c3ea245facf 100644
--- a/packages/start-plugin-core/tests/compiler.test.ts
+++ b/packages/start-plugin-core/tests/compiler.test.ts
@@ -515,6 +515,35 @@ describe('compiler handles external import transforms', () => {
})
describe('server function provider module directives', () => {
+ test.each([
+ 'export const Route = { component: Page }',
+ 'export function Route() { return Page() }',
+ 'export default function Route() { return Page() }',
+ ])(
+ 'removes browser-only dependencies of stripped exports: %s',
+ async (route) => {
+ const compiler = createFullCompiler('server')
+ const result = await compiler.compile({
+ code: `
+ import { createServerFn } from '@tanstack/react-start'
+ export const getMessage = createServerFn().handler(() => 'server message')
+ const browserState = { window }
+ function Page() { return browserState }
+ ${route}
+ `,
+ id: '/test/src/routes/message.tsx?tss-serverfn-split',
+ })
+
+ expect(result).not.toBeNull()
+ expect(result!.code).toContain('server message')
+ expect(result!.code).toContain('createServerRpc')
+ expect(result!.code).not.toContain('window')
+ expect(result!.code).not.toContain('browserState')
+ expect(result!.code).not.toContain('Page')
+ expect(result!.code).not.toContain('Route')
+ },
+ )
+
const code = `
import { createServerFn } from '@tanstack/react-start'
@@ -1266,3 +1295,98 @@ describe('re-export chain resolution', () => {
expect(updatedResult!.code).not.toContain('server-only-value')
})
})
+
+test('leaves unrelated SSR middleware chains to the bundler when a server function factory is present', async () => {
+ const compiler = new StartCompiler({
+ ...getDefaultTestOptions('server'),
+ env: 'server',
+ mode: 'build',
+ lookupKinds: getLookupKindsForEnv('server'),
+ lookupConfigurations: getLookupConfigurationsForEnv('server', 'react'),
+ getKnownServerFns: () => ({}),
+ resolveId: async () => {
+ throw new Error(
+ 'SSR host has no package import conditions for this unrelated binding',
+ )
+ },
+ loadModule: async () => {
+ throw new Error('Unexpected compiler dependency load')
+ },
+ })
+ const code = `import { createMiddleware, createServerFn } from '@tanstack/react-start';
+const middleware = createMiddleware({ type: 'function' }).server(({ next }) => next({ context: { role: 'admin' } }));
+export const factory = createServerFn().middleware([middleware]);`
+ await expect(
+ compiler.compile({
+ id: '/test/auth-wrapper.ts',
+ code,
+ detectedKinds: detectKindsInCode(code, 'server'),
+ }),
+ ).resolves.toBeNull()
+})
+
+test.each([
+ `(createServerOnlyFn)(() => 'server-value')`,
+ `(createIsomorphicFn)().server(() => 'server-value').client(() => 'client-value')`,
+ `(createIsomorphicFn().server(() => 'server-value')).client(() => 'client-value')`,
+])(
+ 'compiles transparent parentheses around native call chains: %s',
+ async (expression) => {
+ const compiler = createFullCompiler('server')
+ const result = await compiler.compile({
+ id: '/test/parenthesized.ts',
+ code: `import { createServerOnlyFn, createIsomorphicFn } from '@tanstack/react-start'; export const fn = ${expression}`,
+ })
+
+ expect(result).not.toBeNull()
+ const evaluate = new Function(
+ `${result!.code.replace(/^import .+;$/gm, '').replace(/\bexport (?=const)/g, '')}\nreturn fn`,
+ )
+ expect(evaluate()()).toBe('server-value')
+ },
+)
+
+test('compiles a server function through a parenthesized namespace receiver', async () => {
+ const compiler = createFullCompiler('client')
+ const result = await compiler.compile({
+ id: '/test/namespace-parenthesized.ts',
+ code: `import * as Start from '@tanstack/react-start'; export const fn = (Start).createServerFn().handler(() => 'private-server-body')`,
+ })
+ expect(result).not.toBeNull()
+ expect(result!.code).not.toContain('private-server-body')
+ expect(result!.code).toContain('createClientRpc')
+})
+
+// The compiler currently mistakes the local parameter for the namespace import.
+// Keep the intended behavior executable until binding resolution is corrected.
+test.fails(
+ 'preserves a shadowed parenthesized namespace receiver',
+ async () => {
+ const compiler = createFullCompiler('client')
+ const code = `import * as Start from '@tanstack/react-start'; export function fn(Start) { return (Start).createServerFn().handler(() => 'local-runtime-body') }`
+ const result = await compiler.compile({
+ id: '/test/shadowed-namespace.ts',
+ code,
+ })
+ const output = result?.code ?? code
+ expect(output).toContain('local-runtime-body')
+ expect(output).not.toContain('createClientRpc')
+ },
+)
+
+test.each([
+ `(Start).createServerOnlyFn(() => 'private-server-body')`,
+ `(Start).createIsomorphicFn().server(() => 'private-server-body').client(() => 'client-value')`,
+ `(Start).createMiddleware().server(() => 'private-server-body')`,
+])(
+ 'removes server-only code through a parenthesized namespace: %s',
+ async (expression) => {
+ const compiler = createFullCompiler('client')
+ const result = await compiler.compile({
+ id: '/test/namespace-builtins.ts',
+ code: `import * as Start from '@tanstack/react-start'; export const fn = ${expression}`,
+ })
+ expect(result).not.toBeNull()
+ expect(result!.code).not.toContain('private-server-body')
+ },
+)
diff --git a/packages/start-plugin-core/tests/hydrate-when-transform.test.ts b/packages/start-plugin-core/tests/hydrate-when-transform.test.ts
index 13e1d909bd1..442482bd86c 100644
--- a/packages/start-plugin-core/tests/hydrate-when-transform.test.ts
+++ b/packages/start-plugin-core/tests/hydrate-when-transform.test.ts
@@ -117,6 +117,94 @@ function loadVirtualHydrateModule(options: {
}
describe('Hydrate compiler transform', () => {
+ test.each(['client', 'server'] as const)(
+ 'resolves spread props declared after their containing function in %s output',
+ (env) => {
+ const result = compileHydrate({
+ code: `import { Hydrate } from '@tanstack/react-start'
+export function Page() { return child
}
+const props = { when: true, fallback: server-fallback
}`,
+ id,
+ root,
+ env,
+ })
+ expect(result).not.toBeNull()
+ expect(result?.code).toContain('when: true')
+ if (env === 'client') {
+ expect(result?.code).not.toContain('.preload')
+ expect(result?.code).toContain('server-fallback')
+ } else {
+ expect(result?.code).not.toContain('server-fallback')
+ }
+ },
+ )
+
+ test.each(['client', 'server'] as const)(
+ 'respects parenthesized split={false} in %s output',
+ (env) => {
+ const result = compileHydrate({
+ code: `import { Hydrate } from '@tanstack/react-start'; export function Page() { return {() => child
} }`,
+ id,
+ root,
+ env,
+ })
+ expect(result).toBeNull()
+ },
+ )
+
+ test.each(['{...({ fallback: server-fallback
})}', '{...(props)}'])(
+ 'strips fallback from a parenthesized spread %s',
+ (spread) => {
+ const result = compileHydrate({
+ code: `import { Hydrate } from '@tanstack/react-start'; ${spread.includes('props') ? 'const props = ({ fallback: server-fallback
});' : ''} export function Page() { return child
}`,
+ id,
+ root,
+ env: 'server',
+ })
+ expect(result).not.toBeNull()
+ expect(result?.code).not.toContain('server-fallback')
+ },
+ )
+
+ test('strips a statically computed fallback property from server output', () => {
+ const result = compileHydrate({
+ code: `import { Hydrate } from '@tanstack/react-start'; export function Page() { return server-fallback
}}>child
}`,
+ id,
+ root,
+ env: 'server',
+ })
+ expect(result).not.toBeNull()
+ expect(result?.code).not.toContain('server-fallback')
+ })
+
+ test.each(['{...({ when: true })}', '{...(props)}'])(
+ 'avoids generating a preload callback for known props %s',
+ (spread) => {
+ const result = compileHydrate({
+ code: `import { Hydrate } from '@tanstack/react-start'; const props = ({ when: true }); export function Page() { return child
}`,
+ id,
+ root,
+ env: 'client',
+ })
+ expect(result).not.toBeNull()
+ expect(result?.code).not.toContain('.preload')
+ },
+ )
+
+ test.each([
+ ['{(() => child
)}', /function-as-children/],
+ ['{(useThing)()}', /hooks/],
+ ])('rejects parenthesized unsafe extraction %s', (child, message) => {
+ expect(() =>
+ compileHydrate({
+ code: `import { Hydrate } from '@tanstack/react-start'; export function Page() { return ${child} }`,
+ id,
+ root,
+ env: 'client',
+ }),
+ ).toThrow(message)
+ })
+
test('splits Hydrate children behind a lazy import', () => {
const result = compileHydrate({
code: `
diff --git a/packages/start-plugin-core/tests/hydrateWhen/hydrateWhen.test.ts b/packages/start-plugin-core/tests/hydrateWhen/hydrateWhen.test.ts
index 9e393a93c77..a027fac5057 100644
--- a/packages/start-plugin-core/tests/hydrateWhen/hydrateWhen.test.ts
+++ b/packages/start-plugin-core/tests/hydrateWhen/hydrateWhen.test.ts
@@ -1,4 +1,5 @@
import { readFile, readdir } from 'node:fs/promises'
+import { traverse } from '@babel/core'
import * as t from '@babel/types'
import { generateFromAst, parseAst } from '@tanstack/router-utils'
import path from 'pathe'
@@ -148,6 +149,63 @@ describe('Hydrate compiler transform fixtures', async () => {
})
})
+ test('generated lazy names do not shadow existing global references', () => {
+ const code = `import { Hydrate } from '@tanstack/react-start'; export const before = _H0; export function Page() { return }`
+ const compiled = compile({
+ env: 'client',
+ code,
+ id: fixtureId('global.tsx'),
+ })!
+ const output = parseAst({ code: compiled.code })
+ const globalReferences: Array = []
+ traverse(output, {
+ ReferencedIdentifier(path) {
+ if (path.node.name === '_H0' && !path.scope.hasBinding('_H0', true)) {
+ globalReferences.push(path.node.name)
+ }
+ },
+ })
+ expect(globalReferences).toEqual(['_H0'])
+ })
+
+ test('retains captured local components and values across extraction', async () => {
+ const code = `
+ import { Hydrate } from '@tanstack/react-start'
+ export function Page({ name }) {
+ const count = 1
+ const Local = () => {name}
+ return {name}
+ }
+ `
+ const id = fixtureId('captured.tsx')
+ const compiled = compile({ env: 'client', code, id })!
+ expect(compiled.code).toContain('Local={Local}')
+ expect(compiled.code).toContain('count={count}')
+ expect(compiled.code).toContain('name={name}')
+ expect(compiled.code).toContain('const count = 1')
+ expect(compiled.code).toContain('const Local')
+ const loaded = await loadVirtualHydrateModule({
+ code,
+ id: virtualHydrateId(id, compiled.boundaries[0]!),
+ root: fixtureRoot,
+ })
+ expect(loaded).toBeTruthy()
+ const output = parseAst({ code: loaded!.code })
+ const capturedReferences = new Set()
+ traverse(output, {
+ Program(path) {
+ expect(path.scope.getBinding('Page')).toBeUndefined()
+ },
+ ReferencedIdentifier(path) {
+ if (['Local', 'count', 'name'].includes(path.node.name)) {
+ capturedReferences.add(path.node.name)
+ expect(path.scope.hasBinding(path.node.name, true)).toBe(true)
+ }
+ },
+ })
+ expect([...capturedReferences].sort()).toEqual(['Local', 'count', 'name'])
+ })
+
test('should extract virtual modules and keep nested ids stable', async () => {
const filename = 'hydrateWhenNested.tsx'
const code = await readFixture(filename)
diff --git a/packages/start-plugin-core/tests/importProtection/analysis.exports.test.ts b/packages/start-plugin-core/tests/importProtection/analysis.exports.test.ts
index dbb0830af7c..1e14f5fd801 100644
--- a/packages/start-plugin-core/tests/importProtection/analysis.exports.test.ts
+++ b/packages/start-plugin-core/tests/importProtection/analysis.exports.test.ts
@@ -4,6 +4,7 @@ import {
isValidExportName,
getMockExportNamesBySource,
getNamedExports,
+ getImportSources,
} from '../../src/import-protection/analysis'
describe('isValidExportName', () => {
@@ -245,3 +246,16 @@ describe('collectNamedExports', () => {
expect(getNamedExports(code)).toEqual(['foo-bar'])
})
})
+
+test('discovers mock exports through parenthesized namespace access', () => {
+ const exports = getMockExportNamesBySource(
+ `import * as denied from 'denied'; (denied).read(); (denied)['write']()`,
+ )
+ expect(exports.get('denied')).toEqual(['read', 'write'])
+})
+
+test('discovers parenthesized literal dynamic import sources', () => {
+ expect(getImportSources(`export const value = import(('denied'))`)).toEqual([
+ 'denied',
+ ])
+})
diff --git a/packages/start-plugin-core/tests/importProtection/analysis.usage.test.ts b/packages/start-plugin-core/tests/importProtection/analysis.usage.test.ts
index 69642e890ab..8db2c2db6cf 100644
--- a/packages/start-plugin-core/tests/importProtection/analysis.usage.test.ts
+++ b/packages/start-plugin-core/tests/importProtection/analysis.usage.test.ts
@@ -53,7 +53,7 @@ describe('findPostCompileUsagePos', () => {
test('ignores binding positions (variable declarator id)', () => {
const p = pos(
- `import { x } from 'denied';\nconst x = 1;\nconst y = x;`,
+ `import { x } from 'denied';\n{ const x = 1;\nconst y = x; }`,
'denied',
)
expect(p).toBeUndefined()
@@ -279,3 +279,38 @@ describe('findOriginalUnsafeUsagePos', () => {
expect(p).toBeUndefined()
})
})
+
+test.each([
+ `createServerFn().handler((() => denied()))`,
+ `(createServerFn)().handler(() => denied())`,
+])(
+ 'preserves safe-boundary recognition through parentheses: %s',
+ (expression) => {
+ const code = `import { denied } from 'denied';\nexport const result = ${expression}`
+ expect(findOriginalUnsafeUsagePos(code, 'denied', 'client')).toBeUndefined()
+ },
+)
+
+test.each([
+ `createIsomorphicFn().client((() => denied()))`,
+ `(createClientOnlyFn)((() => denied()))`,
+])('preserves server safe boundaries through parentheses: %s', (expression) => {
+ const code = `import { denied } from 'denied';\nexport const result = ${expression}`
+ expect(findOriginalUnsafeUsagePos(code, 'denied', 'server')).toBeUndefined()
+})
+
+test('does not treat an immediately invoked function as a safe handler argument', () => {
+ const code = `import { denied } from 'denied';\nexport const result = createServerFn().handler((() => denied())())`
+ expect(findOriginalUnsafeUsagePos(code, 'denied', 'client')).toEqual({
+ line: 2,
+ column0: code.split('\n')[1]!.indexOf('denied'),
+ })
+})
+
+test('prefers parenthesized call usage over an earlier value reference', () => {
+ const code = `import { denied } from 'denied';\nconst value = denied;\n(denied)()`
+ expect(findPostCompileUsagePos(code, 'denied')).toEqual({
+ line: 3,
+ column0: 1,
+ })
+})