Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
39d1efa
test(solid-start): serialization-adapters suite
birkskyum Oct 20, 2025
6df6f56
add hydration
birkskyum Oct 21, 2025
230b4ff
fix lockfile
birkskyum Oct 22, 2025
12ad4aa
add ssrLoadFrom initial
birkskyum Oct 22, 2025
62807c4
use initla only on server
birkskyum Oct 22, 2025
8143d15
check on TSR_DEFERRED_PROMISE
birkskyum Oct 22, 2025
a6d4655
more clear client/server split
birkskyum Oct 22, 2025
e9719e8
reset awaited
birkskyum Oct 22, 2025
291e2d4
defer stream
birkskyum Oct 22, 2025
ec5dd8b
call signal
birkskyum Oct 22, 2025
4050d97
remove deferStream
birkskyum Oct 22, 2025
860ab37
Revert "remove deferStream"
birkskyum Oct 22, 2025
9fc9745
simpler awaited
birkskyum Oct 22, 2025
333ed61
use loweracse charset
birkskyum Oct 22, 2025
706f2de
lockfile
birkskyum Oct 23, 2025
44f513b
update to 1.9.9
birkskyum Oct 23, 2025
349dedb
install
birkskyum Oct 23, 2025
61be233
bump tailwind to v4
birkskyum Oct 23, 2025
1835e59
reset awaited
birkskyum Oct 23, 2025
a954ccc
add routes
birkskyum Oct 23, 2025
77310bb
add back index
birkskyum Oct 23, 2025
ec14ecb
Revert "reset awaited"
birkskyum Oct 23, 2025
eb19e59
add seroval to pnpm overrides
birkskyum Oct 23, 2025
3e7d446
add error component
birkskyum Oct 23, 2025
333ae5a
add synx/async to the transformer
birkskyum Oct 23, 2025
20d7aaa
bump seroval
birkskyum Oct 23, 2025
6357bab
seroval 1.3.2
birkskyum Oct 23, 2025
265ff5e
add not found component
birkskyum Oct 23, 2025
7479380
pass seroval plugins down to solid ssr
birkskyum Oct 23, 2025
14dd994
revert changes to router-core
birkskyum Oct 27, 2025
fdb188c
lcokfile
birkskyum Oct 27, 2025
6f0b1d1
also add serialization plugins to renderRouterToString
birkskyum Oct 27, 2025
199c360
ci: apply automated fixes
autofix-ci[bot] Oct 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions e2e/solid-start/serialization-adapters/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
node_modules
package-lock.json
yarn.lock

.DS_Store
.cache
.env
.vercel
.output
/build/
/api/
/server/build
/public/build# Sentry Config File
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Fix formatting: separate pattern and comment.

The pattern /public/build and the comment # Sentry Config File are on the same line. This makes the comment ineffective and could be confusing.

-/public/build# Sentry Config File
+/public/build
+# Sentry Config File
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/public/build# Sentry Config File
/public/build
# Sentry Config File
🤖 Prompt for AI Agents
In e2e/solid-start/serialization-adapters/.gitignore around line 13, the
gitignore entry combines the pattern "/public/build" and the comment "# Sentry
Config File" on the same line; split them so the ignore pattern stands alone on
its own line and the explanatory comment is on a separate line (e.g., move the
comment to the line above or below the pattern).

.env.sentry-build-plugin
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

count.txt
4 changes: 4 additions & 0 deletions e2e/solid-start/serialization-adapters/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/build
**/public
pnpm-lock.yaml
routeTree.gen.ts
33 changes: 33 additions & 0 deletions e2e/solid-start/serialization-adapters/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "tanstack-solid-start-e2e-serialization-adapters",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"dev:e2e": "vite dev",
"build": "vite build && tsc --noEmit",
"start": "pnpx srvx --prod -s ../client dist/server/server.js",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the client directory path in the start script.

The path -s ../client appears to reference a directory outside the current e2e test package. For an e2e test suite, the client build artifacts should typically be within the package's own build directory (e.g., dist/client or similar).

Run the following script to check if the referenced path exists and verify the intended directory structure:


🏁 Script executed:

#!/bin/bash
# Description: Verify client directory structure and start script paths

# Check if ../client exists relative to this package
if [ -d "e2e/solid-start/serialization-adapters/../client" ]; then
  echo "Found: e2e/solid-start/serialization-adapters/../client"
  ls -la e2e/solid-start/serialization-adapters/../client
else
  echo "Not found: e2e/solid-start/serialization-adapters/../client"
fi

# Look for client build directories within this package
fd -t d "client" e2e/solid-start/serialization-adapters/

# Check for dist or build directories that might contain client artifacts
fd -t d "^(dist|build)$" e2e/solid-start/serialization-adapters/ --max-depth 2

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Description: Understand e2e/solid-start project structure and client reference

# List all directories in e2e/solid-start
echo "=== Directory structure of e2e/solid-start ==="
find e2e/solid-start -type d -not -path '*/node_modules/*' | head -20

# Find all package.json files to understand packages in e2e/solid-start
echo -e "\n=== Package.json files in e2e/solid-start ==="
fd -e "package.json" e2e/solid-start/ --max-depth 3

# Look for any 'client' directories anywhere in e2e or root
echo -e "\n=== Any 'client' directories in e2e/solid-start ==="
fd -t d "client" e2e/solid-start/ --max-depth 5

# Check if there's a client at root level
echo -e "\n=== Check root level directories ==="
ls -d */ 2>/dev/null | grep -E "(client|packages|apps)" || echo "No obvious client/packages/apps at root"

# Look at what the start script reference file contains and its context
echo -e "\n=== Full content of the package.json under review ==="
cat e2e/solid-start/serialization-adapters/package.json

Length of output: 2402


🏁 Script executed:

#!/bin/bash
# Description: Find all package.json, check build outputs, and understand project structure

# Find all package.json files properly
echo "=== All package.json files in e2e/solid-start ==="
fd --full-path "package.json" e2e/solid-start/

# Check if there's a build/dist directory with client artifacts
echo -e "\n=== Check for dist directories in serialization-adapters ==="
ls -la e2e/solid-start/serialization-adapters/ 2>/dev/null | grep -E "^d" || echo "No directories listed"

# Look for what exists at the parent level (e2e/solid-start)
echo -e "\n=== Contents of e2e/solid-start parent level ==="
ls -la e2e/solid-start/ | grep -E "^d" | head -20

# Check if there's a client directory elsewhere (e.g., in packages)
echo -e "\n=== Look for 'client' in packages/ ==="
find packages/ -type d -name "client" 2>/dev/null || echo "No client directories in packages/"

# Check other e2e packages' start scripts for comparison
echo -e "\n=== Start scripts in other e2e packages ==="
rg -A 1 '"start"' e2e/solid-start/*/package.json

# Check the build output structure by looking at vite.config or tsconfig
echo -e "\n=== Check vite.config in serialization-adapters ==="
ls -la e2e/solid-start/serialization-adapters/ | grep -i vite || echo "No vite.config found in listing"
fd "vite.config" e2e/solid-start/serialization-adapters/

Length of output: 5693


🏁 Script executed:

#!/bin/bash
# Description: Understand vite build output and srvx -s flag behavior

# Check vite.config.ts in serialization-adapters
echo "=== vite.config.ts in serialization-adapters ==="
cat e2e/solid-start/serialization-adapters/vite.config.ts

# Check if there's any build setup or shared client directory mentioned
echo -e "\n=== Check root vite.config or build config ==="
fd "vite.config" . --max-depth 1
fd "build" . --max-depth 1 -t f

# Look at package.json root for any build/setup scripts
echo -e "\n=== Root package.json scripts ==="
rg '"scripts"' -A 30 package.json | head -40

# Search for documentation or comments about srvx or ../client pattern
echo -e "\n=== Search for srvx or client path references ==="
rg "srvx" --type json --type ts --type js -B 2 -A 2 e2e/solid-start/basic/

# Check the vite build output by looking at what other e2e packages have
echo -e "\n=== Check dist structure in e2e/solid-start/basic after potential build ==="
ls -la e2e/solid-start/basic/dist 2>/dev/null || echo "No dist directory in basic"

# Look for srvx documentation or usage in the repo
echo -e "\n=== Search for srvx in entire repo ==="
rg "srvx" -t json | head -10

Length of output: 4497


🌐 Web query:

srvx npm package -s flag documentation production server

💡 Result:

The -s shorthand is the CLI's "silent" option (same as --silent) — it suppresses the server listening message/console output. See the Server Options docs (silent option). [1][2]

Sources:

  • srvx Server Options — "silent: If enabled, no server listening message will be printed." [1]
  • srvx home / CLI quickstart (shows npx srvx usage). [2]

"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
},
"dependencies": {
"@tanstack/solid-router": "workspace:^",
"@tanstack/solid-router-devtools": "workspace:^",
"@tanstack/solid-start": "workspace:^",
"solid-js": "^1.9.10",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4",
"zod": "^3.24.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.15",
"@tanstack/router-e2e-utils": "workspace:^",
"@types/node": "^22.10.2",
"postcss": "^8.5.1",
"seroval": "^1.3.2",
"srvx": "^0.8.6",
"tailwindcss": "^4.1.15",
"typescript": "^5.7.2",
"vite-plugin-solid": "^2.11.9"
}
}
34 changes: 34 additions & 0 deletions e2e/solid-start/serialization-adapters/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { defineConfig, devices } from '@playwright/test'
import { getTestServerPort } from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

const PORT = await getTestServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
workers: 1,

reporter: [['line']],

use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL,
},

webServer: {
command: `VITE_SERVER_PORT=${PORT} pnpm build && NODE_ENV=production PORT=${PORT} VITE_SERVER_PORT=${PORT} pnpm start`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
},

projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
})
5 changes: 5 additions & 0 deletions e2e/solid-start/serialization-adapters/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
31 changes: 31 additions & 0 deletions e2e/solid-start/serialization-adapters/src/CustomError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createSerializationAdapter } from '@tanstack/solid-router'

export class CustomError extends Error {
public foo: string
public bar: bigint

constructor(message: string, options: { foo: string; bar: bigint }) {
super(message)

Object.setPrototypeOf(this, new.target.prototype)

this.name = this.constructor.name
this.foo = options.foo
this.bar = options.bar
}
}

export const customErrorAdapter = createSerializationAdapter({
key: 'custom-error',
test: (v) => v instanceof CustomError,
toSerializable: ({ message, foo, bar }) => {
return {
message,
foo,
bar,
}
},
fromSerializable: ({ message, foo, bar }) => {
return new CustomError(message, { foo, bar })
},
})
Comment on lines +21 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

BigInt is not JSON-serializable; stringify in toSerializable and rehydrate in fromSerializable.

Avoid runtime TypeError during transport by serializing bar as a string and parsing back to bigint.

-export const customErrorAdapter = createSerializationAdapter({
+export const customErrorAdapter = createSerializationAdapter<
+  CustomError,
+  { message: string; foo: string; bar: string }
+>({
   key: 'custom-error',
-  test: (v) => v instanceof CustomError,
-  toSerializable: ({ message, foo, bar }) => {
-    return {
-      message,
-      foo,
-      bar,
-    }
-  },
-  fromSerializable: ({ message, foo, bar }) => {
-    return new CustomError(message, { foo, bar })
-  },
+  test: (v: unknown): v is CustomError => v instanceof CustomError,
+  toSerializable: ({ message, foo, bar }) => ({
+    message,
+    foo,
+    bar: bar.toString(),
+  }),
+  fromSerializable: ({ message, foo, bar }) =>
+    new CustomError(message, { foo, bar: BigInt(bar) }),
 })

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In e2e/solid-start/serialization-adapters/src/CustomError.ts around lines 21 to
31, the current serializer returns bar as a bigint which will cause
JSON.stringify to throw; modify toSerializable to convert bar to a string (e.g.
String(bar) or bar?.toString()) and modify fromSerializable to rehydrate by
calling BigInt(parsedBar) (handling null/undefined/empty values appropriately)
so bar is safely transported as a string and restored back to a bigint when
reconstructing the CustomError.

205 changes: 205 additions & 0 deletions e2e/solid-start/serialization-adapters/src/data.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { createSerializationAdapter } from '@tanstack/solid-router'

export class Foo {
constructor(public value: string) {}
}

export interface Car {
__type: 'car'
make: string
model: string
year: number
honk: () => { message: string; make: string; model: string; year: number }
}

export function makeCar(opts: {
make: string
model: string
year: number
}): Car {
return {
...opts,
__type: 'car',
honk: () => {
return { message: `Honk! Honk!`, ...opts }
},
}
}

export const fooAdapter = createSerializationAdapter({
key: 'foo',
test: (value: any) => value instanceof Foo,
toSerializable: (foo) => foo.value,
fromSerializable: (value) => new Foo(value),
})

export const carAdapter = createSerializationAdapter({
key: 'car',
test: (value: any): value is Car =>
'__type' in (value as Car) && value.__type === 'car',
Comment on lines +37 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard against null/primitive in adapter test to avoid runtime TypeError.

'__type' in value throws for null/undefined. Add typeof/null checks.

 export const carAdapter = createSerializationAdapter({
   key: 'car',
-  test: (value: any): value is Car =>
-    '__type' in (value as Car) && value.__type === 'car',
+  test: (value: unknown): value is Car =>
+    typeof value === 'object' &&
+    value !== null &&
+    '__type' in (value as Record<string, unknown>) &&
+    (value as any).__type === 'car',
🤖 Prompt for AI Agents
In e2e/solid-start/serialization-adapters/src/data.tsx around lines 37 to 39,
the adapter test uses "'__type' in (value as Car)" which will throw for
null/undefined or primitives; change the guard to first ensure value is a
non-null object (e.g., value !== null && typeof value === 'object') before
checking the '__type' property and comparing it to 'car' so the type guard
safely returns false for null/primitive inputs.

toSerializable: (car) => ({
make: car.make,
model: car.model,
year: car.year,
}),
fromSerializable: (value: { make: string; model: string; year: number }) =>
makeCar(value),
})

export function makeData() {
function makeFoo(suffix: string = '') {
return new Foo(typeof window === 'undefined' ? 'server' : 'client' + suffix)
}
return {
foo: {
singleInstance: makeFoo(),
array: [makeFoo('0'), makeFoo('1'), makeFoo('2')],
map: new Map([
[0, makeFoo('0')],
[1, makeFoo('1')],
[2, makeFoo('2')],
]),
mapOfArrays: new Map([
[0, [makeFoo('0-a'), makeFoo('0-b')]],
[1, [makeFoo('1-a'), makeFoo('1-b')]],
[2, [makeFoo('2-a'), makeFoo('2-b')]],
]),
},
car: {
singleInstance: makeCar({
make: 'Toyota',
model: 'Camry',
year: 2020,
}),
array: [
makeCar({ make: 'Honda', model: 'Accord', year: 2019 }),
makeCar({ make: 'Ford', model: 'Mustang', year: 2021 }),
],
map: new Map([
[0, makeCar({ make: 'Chevrolet', model: 'Malibu', year: 2018 })],
[1, makeCar({ make: 'Nissan', model: 'Altima', year: 2020 })],
[2, makeCar({ make: 'Hyundai', model: 'Sonata', year: 2021 })],
]),
mapOfArrays: new Map([
[0, [makeCar({ make: 'Kia', model: 'Optima', year: 2019 })]],
[1, [makeCar({ make: 'Subaru', model: 'Legacy', year: 2020 })]],
[2, [makeCar({ make: 'Volkswagen', model: 'Passat', year: 2021 })]],
]),
},
}
}
export class NestedOuter {
constructor(public inner: NestedInner) {}
whisper() {
return this.inner.value.toLowerCase()
}
}

export class NestedInner {
constructor(public value: string) {}
shout() {
return this.value.toUpperCase()
}
}

export const nestedInnerAdapter = createSerializationAdapter({
key: 'nestedInner',
test: (value): value is NestedInner => value instanceof NestedInner,
toSerializable: (inner) => inner.value,
fromSerializable: (value) => new NestedInner(value),
})

export const nestedOuterAdapter = createSerializationAdapter({
key: 'nestedOuter',
extends: [nestedInnerAdapter],
test: (value) => value instanceof NestedOuter,
toSerializable: (outer) => outer.inner,
fromSerializable: (value) => new NestedOuter(value),
})

export function makeNested() {
return new NestedOuter(new NestedInner('Hello World'))
}

export function RenderData({
id,
data,
}: {
id: string
data: ReturnType<typeof makeData>
}) {
const localData = makeData()
return (
<div data-testid={`${id}-container`}>
<h3>Car</h3>
<h4>expected</h4>
<div data-testid={`${id}-car-expected`}>
{JSON.stringify({
make: localData.car.singleInstance.make,
model: localData.car.singleInstance.model,
year: localData.car.singleInstance.year,
})}
</div>
<h4>actual</h4>
<div data-testid={`${id}-car-actual`}>
{JSON.stringify({
make: data.car.singleInstance.make,
model: data.car.singleInstance.model,
year: data.car.singleInstance.year,
})}
</div>
<b>Foo</b>
<div data-testid={`${id}-foo`}>
{JSON.stringify({
value: data.foo.singleInstance.value,
})}
</div>
</div>
)
}

export function RenderNestedData({ nested }: { nested: NestedOuter }) {
{
const localData = makeNested()
const expectedShoutState = localData.inner.shout()
const expectedWhisperState = localData.whisper()
const shoutState = nested.inner.shout()
const whisperState = nested.whisper()

return (
<div data-testid="data-only-container">
<h2 data-testid="data-only-heading">data-only</h2>
<div data-testid="shout-container">
<h3>shout</h3>
<div>
expected:{' '}
<div data-testid="shout-expected-state">
{JSON.stringify(expectedShoutState)}
</div>
</div>
<div>
actual:{' '}
<div data-testid="shout-actual-state">
{JSON.stringify(shoutState)}
</div>
</div>
</div>
<div data-testid="whisper-container">
<h3>whisper</h3>
<div>
expected:{' '}
<div data-testid="whisper-expected-state">
{JSON.stringify(expectedWhisperState)}
</div>
</div>
<div>
actual:{' '}
<div data-testid="whisper-actual-state">
{JSON.stringify(whisperState)}
</div>
</div>
</div>
</div>
)
}
}
Loading
Loading