Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion e2e/solid-router/basepath-file-based/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "tanstack-solid-router-e2e-react-basepath-file-based",
"name": "tanstack-router-e2e-solid-basepath-file-based",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion e2e/solid-router/generator-cli-only/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "tanstack-solid-router-e2e-react-generator-cli-only",
"name": "tanstack-router-e2e-solid-generator-cli-only",
"private": true,
"type": "module",
"scripts": {
Expand Down
11 changes: 11 additions & 0 deletions e2e/solid-router/js-only-file-based/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules
.DS_Store
dist
dist-hash
dist-ssr
*.local

/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
12 changes: 12 additions & 0 deletions e2e/solid-router/js-only-file-based/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions e2e/solid-router/js-only-file-based/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "tanstack-router-e2e-solid-js-only-file-based",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"dev:e2e": "vite",
"build": "vite build && tsc --noEmit",
"serve": "vite preview",
"start": "vite",
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.15",
"@tanstack/solid-router": "workspace:^",
"@tanstack/solid-router-devtools": "workspace:^",
"postcss": "^8.5.1",
"solid-js": "^1.9.9",
"redaxios": "^0.5.1",
"tailwindcss": "^4.1.15"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
"@tanstack/router-plugin": "workspace:^",
"vite-plugin-solid": "^2.11.10",
"vite": "^7.1.7"
}
}
42 changes: 42 additions & 0 deletions e2e/solid-router/js-only-file-based/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { defineConfig, devices } from '@playwright/test'
import {
getDummyServerPort,
getTestServerPort,
} from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

const PORT = await getTestServerPort(packageJson.name)
const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
workers: 1,

reporter: [['line']],

globalSetup: './tests/setup/global.setup.ts',
globalTeardown: './tests/setup/global.teardown.ts',

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

webServer: {
command: `VITE_NODE_ENV="test" VITE_EXTERNAL_PORT=${EXTERNAL_PORT} VITE_SERVER_PORT=${PORT} pnpm build && pnpm serve --port ${PORT}`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
},

projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
})
5 changes: 5 additions & 0 deletions e2e/solid-router/js-only-file-based/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
18 changes: 18 additions & 0 deletions e2e/solid-router/js-only-file-based/src/main.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { RouterProvider, createRouter } from '@tanstack/solid-router'
import { routeTree } from './routeTree.gen'
import { render } from 'solid-js/web'
import './styles.css'

// Set up a Router instance
const router = createRouter({
routeTree,
defaultPreload: 'intent',
defaultStaleTime: 5000,
scrollRestoration: true,
})

const rootElement = document.getElementById('app')

if (!rootElement.innerHTML) {
render(() => <RouterProvider router={router} />, rootElement)
}
Comment on lines +14 to +18
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

Add null safety check for rootElement.

Accessing .innerHTML on a potentially null element will throw a runtime error if the #app element doesn't exist in the DOM.

Apply this diff to add proper null safety:

 const rootElement = document.getElementById('app')

-if (!rootElement.innerHTML) {
+if (rootElement && !rootElement.innerHTML) {
   render(() => <RouterProvider router={router} />, rootElement)
 }
📝 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
const rootElement = document.getElementById('app')
if (!rootElement.innerHTML) {
render(() => <RouterProvider router={router} />, rootElement)
}
const rootElement = document.getElementById('app')
if (rootElement && !rootElement.innerHTML) {
render(() => <RouterProvider router={router} />, rootElement)
}
🤖 Prompt for AI Agents
In e2e/solid-router/js-only-file-based/src/main.jsx around lines 14 to 18, the
code accesses rootElement.innerHTML without verifying rootElement is non-null;
update the logic to first check whether rootElement exists and only then access
innerHTML (e.g., if rootElement is null, return or throw a clear error), and
ensure the render call is executed only when rootElement is present and its
innerHTML is empty.

27 changes: 27 additions & 0 deletions e2e/solid-router/js-only-file-based/src/posts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import axios from 'redaxios'

export class NotFoundError extends Error {}

let queryURL = 'https://jsonplaceholder.typicode.com'

if (import.meta.env.VITE_NODE_ENV === 'test') {
queryURL = `http://localhost:${import.meta.env.VITE_EXTERNAL_PORT}`
}

export const fetchPosts = async () => {
console.info('Fetching posts...')
return axios.get(`${queryURL}/posts`).then((r) => r.data.slice(0, 10))
}

export const fetchPost = async (postId) => {
console.info(`Fetching post with id ${postId}...`)
const post = await axios
.get(`${queryURL}/posts/${postId}`)
.then((r) => r.data)

if (!post) {
throw new NotFoundError(`Post with id "${postId}" not found!`)
}
Comment on lines +22 to +24
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

Strengthen the post validation logic.

The check if (!post) may not catch all invalid responses. If the API returns an empty object {} or an object without expected properties, the validation will fail.

Apply this diff to add more robust validation:

-  if (!post) {
+  if (!post || !post.id) {
     throw new NotFoundError(`Post with id "${postId}" not found!`)
   }

Or use more comprehensive validation:

-  if (!post) {
+  if (!post || typeof post !== 'object' || !post.id || !post.title) {
     throw new NotFoundError(`Post with id "${postId}" not found!`)
   }
📝 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
if (!post) {
throw new NotFoundError(`Post with id "${postId}" not found!`)
}
if (!post || !post.id) {
throw new NotFoundError(`Post with id "${postId}" not found!`)
}
🤖 Prompt for AI Agents
In e2e/solid-router/js-only-file-based/src/posts.js around lines 22 to 24, the
current validation uses if (!post) which misses cases like an empty object or
objects missing expected fields; update the validation to verify the post has
the required properties and types (e.g., post.id and post.title or whichever
fields your app expects) and throw NotFoundError when any required property is
absent or invalid; alternatively, integrate a lightweight runtime schema check
(e.g., a small utility or a Zod/validator call) to assert shape and types before
returning the post.


return post
}
95 changes: 95 additions & 0 deletions e2e/solid-router/js-only-file-based/src/routeTree.gen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* eslint-disable */

// @ts-nocheck

// noinspection JSUnusedGlobalSymbols

// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
import { Route as PostsRouteRouteImport } from './routes/posts.route'
import { Route as IndexRouteImport } from './routes/index'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'

const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
id: '/_pathlessLayout',
getParentRoute: () => rootRouteImport,
})
const PostsRouteRoute = PostsRouteRouteImport.update({
id: '/posts',
path: '/posts',
getParentRoute: () => rootRouteImport,
})
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
})
const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PostsRouteRoute,
})
const PostsPostIdRoute = PostsPostIdRouteImport.update({
id: '/$postId',
path: '/$postId',
getParentRoute: () => PostsRouteRoute,
})
const PathlessLayoutNestedLayoutRoute =
PathlessLayoutNestedLayoutRouteImport.update({
id: '/_nested-layout',
getParentRoute: () => PathlessLayoutRoute,
})
const PathlessLayoutNestedLayoutRouteBRoute =
PathlessLayoutNestedLayoutRouteBRouteImport.update({
id: '/route-b',
path: '/route-b',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
})
const PathlessLayoutNestedLayoutRouteARoute =
PathlessLayoutNestedLayoutRouteARouteImport.update({
id: '/route-a',
path: '/route-a',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
})

const PostsRouteRouteChildren = {
PostsPostIdRoute: PostsPostIdRoute,
PostsIndexRoute: PostsIndexRoute,
}

const PostsRouteRouteWithChildren = PostsRouteRoute._addFileChildren(
PostsRouteRouteChildren,
)

const PathlessLayoutNestedLayoutRouteChildren = {
PathlessLayoutNestedLayoutRouteARoute: PathlessLayoutNestedLayoutRouteARoute,
PathlessLayoutNestedLayoutRouteBRoute: PathlessLayoutNestedLayoutRouteBRoute,
}

const PathlessLayoutNestedLayoutRouteWithChildren =
PathlessLayoutNestedLayoutRoute._addFileChildren(
PathlessLayoutNestedLayoutRouteChildren,
)

const PathlessLayoutRouteChildren = {
PathlessLayoutNestedLayoutRoute: PathlessLayoutNestedLayoutRouteWithChildren,
}

const PathlessLayoutRouteWithChildren = PathlessLayoutRoute._addFileChildren(
PathlessLayoutRouteChildren,
)

const rootRouteChildren = {
IndexRoute: IndexRoute,
PostsRouteRoute: PostsRouteRouteWithChildren,
PathlessLayoutRoute: PathlessLayoutRouteWithChildren,
}
export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)
61 changes: 61 additions & 0 deletions e2e/solid-router/js-only-file-based/src/routes/__root.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Link, Outlet, createRootRoute } from '@tanstack/solid-router'
import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'

export const Route = createRootRoute({
component: RootComponent,
notFoundComponent: () => {
return (
<div>
<p>This is the notFoundComponent configured on root route</p>
<Link to="/">Start Over</Link>
</div>
)
},
})

function RootComponent() {
return (
<>
<div class="p-2 flex gap-2 text-lg border-b">
<Link
to="/"
activeProps={{
class: 'font-bold',
}}
activeOptions={{ exact: true }}
>
Home
</Link>{' '}
<Link
to="/posts"
activeProps={{
class: 'font-bold',
}}
>
Posts
</Link>{' '}
<Link
to="/route-a"
activeProps={{
class: 'font-bold',
}}
>
Pathless Layout
</Link>{' '}
<Link
// @ts-expect-error
to="/this-route-does-not-exist"
activeProps={{
class: 'font-bold',
}}
>
This Route Does Not Exist
</Link>
</div>
<hr />
<Outlet />
{/* Start rendering router matches */}
<TanStackRouterDevtools position="bottom-right" />
</>
)
}
17 changes: 17 additions & 0 deletions e2e/solid-router/js-only-file-based/src/routes/_pathlessLayout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createFileRoute } from '@tanstack/solid-router'
import { Outlet } from '@tanstack/solid-router'

export const Route = createFileRoute('/_pathlessLayout')({
component: LayoutComponent,
})

function LayoutComponent() {
return (
<div class="p-2">
<div class="border-b">I'm a pathless layout</div>
<div>
<Outlet />
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createFileRoute } from '@tanstack/solid-router'
import { Link, Outlet } from '@tanstack/solid-router'

export const Route = createFileRoute('/_pathlessLayout/_nested-layout')({
component: LayoutComponent,
})

function LayoutComponent() {
return (
<div>
<div>I'm a nested pathless layout</div>
<div class="flex gap-2 border-b">
<Link
to="/route-a"
activeProps={{
class: 'font-bold',
}}
>
Go to route A
</Link>
<Link
to="/route-b"
activeProps={{
class: 'font-bold',
}}
>
Go to route B
</Link>
</div>
<div>
<Outlet />
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/solid-router'
export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-a')(
{
component: LayoutAComponent,
},
)

function LayoutAComponent() {
return <div>I'm layout A!</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/solid-router'
export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-b')(
{
component: LayoutBComponent,
},
)

function LayoutBComponent() {
return <div>I'm layout B!</div>
}
Loading
Loading