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
12 changes: 12 additions & 0 deletions .changeset/fix-demo-file-leaks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@tanstack/create': patch
---

Fix demo/example files leaking into projects when users opt out of demo pages.

- Strip add-on demo support files in `src/lib/`, `src/hooks/`, `src/data/`, `src/components/`, `src/store/`, and any `demo.*` / `demo-*` / `example.*` / `example-*` files.
- Strip example image assets under `public/`.
- Generate a minimal base starter (no Header, Footer, ThemeToggle, about page, or styled index page) when declining demo/example pages.
- Render Better Auth header-user component as `null` when its demo route is excluded, instead of linking to a non-existent route.

Closes #422, #409.
30 changes: 22 additions & 8 deletions packages/create/src/create-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,23 @@ import { runSpecialSteps } from './special-steps/index.js'

import type { Environment, FileBundleHandler, Options } from './types.js'

function isDemoRoutePath(path?: string) {
function isDemoFilePath(path?: string) {
if (!path) return false
const normalized = path.replace(/\\/g, '/')
return (

if (
normalized.includes('/routes/demo/') ||
normalized.includes('/routes/demo.') ||
normalized.includes('/routes/example/') ||
normalized.includes('/routes/example.')
normalized.includes('/routes/example/')
) {
return true
}

const filename = normalized.split('/').pop() || ''
return (
filename.startsWith('demo.') ||
filename.startsWith('demo-') ||
filename.startsWith('example.') ||
filename.startsWith('example-')
)
}

Expand All @@ -38,20 +47,25 @@ function stripExamplesFromOptions(options: Options): Options {
.map((addOn) => {
const filteredRoutes = (addOn.routes || []).filter(
(route) =>
!isDemoRoutePath(route.path) &&
!isDemoFilePath(route.path) &&
!(route.url && route.url.startsWith('/demo')),
)
Comment on lines 48 to 52
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Also filter /example route URLs when examples are disabled.

stripExamplesFromOptions currently removes URLs prefixed with /demo only. Routes with route.url like /example/... can still pass through in code-router-style add-ons.

Suggested fix
       const filteredRoutes = (addOn.routes || []).filter(
         (route) =>
           !isDemoFilePath(route.path) &&
-          !(route.url && route.url.startsWith('/demo')),
+          !(
+            route.url &&
+            (route.url.startsWith('/demo') || route.url.startsWith('/example'))
+          ),
       )
📝 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 filteredRoutes = (addOn.routes || []).filter(
(route) =>
!isDemoRoutePath(route.path) &&
!isDemoFilePath(route.path) &&
!(route.url && route.url.startsWith('/demo')),
)
const filteredRoutes = (addOn.routes || []).filter(
(route) =>
!isDemoFilePath(route.path) &&
!(
route.url &&
(route.url.startsWith('/demo') || route.url.startsWith('/example'))
),
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/create/src/create-app.ts` around lines 48 - 52, filteredRoutes
currently filters out demo routes using isDemoFilePath and checking
route.url.startsWith('/demo'), but it misses routes with URLs under '/example';
update the filter predicate used when building filteredRoutes (and any related
stripExamplesFromOptions logic) to also exclude route.url that
startsWith('/example') (in addition to '/demo') so addOn.routes entries with
route.url like '/example/...' are removed when examples are disabled; reference
the filteredRoutes variable, addOn.routes, isDemoFilePath, and route.url when
making the change.


const filteredIntegrations = (addOn.integrations || []).filter(
(integration) => !isDemoFilePath(integration.path)
)

return {
...addOn,
routes: filteredRoutes,
integrations: filteredIntegrations,
getFiles: async () => {
const files = await addOn.getFiles()
return files.filter((file) => !isDemoRoutePath(file))
return files.filter((file) => !isDemoFilePath(file))
},
getDeletedFiles: async () => {
const deletedFiles = await addOn.getDeletedFiles()
return deletedFiles.filter((file) => !isDemoRoutePath(file))
return deletedFiles.filter((file) => !isDemoFilePath(file))
},
}
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { authClient } from "#/lib/auth-client";
<%_ if (routes.some(r => r.url === '/demo/better-auth')) { _%>
import { Link } from "@tanstack/react-router";
<%_ } _%>

export default function BetterAuthHeader() {
const { data: session, isPending } = authClient.useSession();
Expand Down Expand Up @@ -34,6 +36,7 @@ export default function BetterAuthHeader() {
);
}

<%_ if (routes.some(r => r.url === '/demo/better-auth')) { _%>
return (
<Link
to="/demo/better-auth"
Expand All @@ -42,4 +45,7 @@ export default function BetterAuthHeader() {
Sign in
</Link>
);
<%_ } else { _%>
return null;
<%_ } _%>
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<% if (!includeExamples) { ignoreFile(); return; } %>
export default function Footer() {
const year = new Date().getFullYear()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<% if (!includeExamples) { ignoreFile(); return; } %>
import { Link } from '@tanstack/react-router'
<% for (const integration of integrations.filter((i) => i.type === 'header-user')) { %>import <%= integration.jsName %> from '<%= relativePath(integration.path) %>'
<% } %>import ThemeToggle from './ThemeToggle'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<% if (!includeExamples) { ignoreFile(); return; } %>
import { useEffect, useState } from 'react'

type ThemeMode = 'light' | 'dark' | 'auto'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,99 @@ function RootComponent() {
</>
)
}
<% } else if (!includeExamples) { %>
<% let hasContext = addOnEnabled["apollo-client"] || addOnEnabled["tanstack-query"]; %>
import {
HeadContent, Scripts, <% if (hasContext) { %>createRootRouteWithContext<% } else { %>createRootRoute<% } %> } from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
import { TanStackDevtools } from '@tanstack/react-devtools'
<% for(const integration of integrations.filter(i => i.type === 'layout' || i.type === 'provider' || i.type === 'devtools')) { %>
import <%= integration.jsName %> from '<%= relativePath(integration.path, true) %>'
<% } %><% if (addOnEnabled.paraglide) { %>
import { getLocale } from '#/paraglide/runtime'
<% } %>
import appCss from '../styles.css?url'
<% if (addOnEnabled["apollo-client"]) { %>
import type { ApolloClientIntegration } from "@apollo/client-integration-tanstack-start";
<% } %>
<% if (addOnEnabled["tanstack-query"]) { %>
import type { QueryClient } from '@tanstack/react-query'
<% if (addOnEnabled.tRPC) { %>
import type { TRPCRouter } from '#/integrations/trpc/router'
import type { TRPCOptionsProxy } from '@trpc/tanstack-react-query'
<% } %>
<% } %>
<% if (hasContext) { %>
interface MyRouterContext <% if (addOnEnabled["apollo-client"]) {%> extends ApolloClientIntegration.RouterContext <%} %>{
<% if (addOnEnabled["tanstack-query"]) { %>
queryClient: QueryClient
<% if (addOnEnabled.tRPC) { %>
trpc: TRPCOptionsProxy<TRPCRouter>
<% } %>
<% } %>
}<% } %>

export const Route = <% if (hasContext) { %>createRootRouteWithContext<MyRouterContext>()<% } else { %>createRootRoute<% } %>({
<% if (addOnEnabled.paraglide) { %>
beforeLoad: async () => {
// Other redirect strategies are possible; see
// https://github.com/TanStack/router/tree/main/examples/react/i18n-paraglide#offline-redirect
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('lang', getLocale())
}
},
<% } %>
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'TanStack Start Starter',
},
],
links: [
{
rel: 'stylesheet',
href: appCss,
},
],
}),
shellComponent: RootDocument
})

function RootDocument({ children }: { children: React.ReactNode }) {
return (
<% if (addOnEnabled.paraglide) { %><html lang={getLocale()}><% } else { %><html lang="en"><% } %>
<head>
<HeadContent />
</head>
<body>
<% for(const integration of integrations.filter(i => i.type === 'provider')) { %><<%= integration.jsName %>>
<% } %>{children}
<TanStackDevtools
config={{
position: 'bottom-right',
}}
plugins={[
{
name: 'Tanstack Router',
render: <TanStackRouterDevtoolsPanel />,
},
<% for(const integration of integrations.filter(i => i.type === 'devtools')) { %><%= integration.jsName %>,<% } %>
]}
/>
<% for(const integration of integrations.filter(i => i.type === 'layout')) { %><<%= integration.jsName %> />
<% } %><% for(const integration of integrations.filter(i => i.type === 'provider').reverse()) { %></<%= integration.jsName %>>
<% } %><Scripts />
</body>
</html>
)
}
<% } else { %>
<% let hasContext = addOnEnabled["apollo-client"] || addOnEnabled["tanstack-query"]; %>
import {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<% if (!includeExamples) { ignoreFile(); return; } %>
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/about')({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
<% if (!includeExamples) { %>
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({ component: Home });

function Home() {
return (
<div className="p-8">
<h1 className="text-4xl font-bold">Welcome to TanStack Start</h1>
<p className="mt-4 text-lg">
Edit <code>src/routes/index.tsx</code> to get started.
</p>
</div>
);
}
<% } else { %>
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({ component: App });
Expand Down Expand Up @@ -70,3 +86,4 @@ function App() {
</main>
);
}
<% } %>
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
<% if (!includeExamples) { %>
@import "tailwindcss";

* {
box-sizing: border-box;
}

html,
body,
#app {
min-height: 100%;
}

body {
margin: 0;
}
<% } else { %>
@import url("https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Manrope:wght@400;500;600;700;800&display=swap");
@import "tailwindcss";
@plugin "@tailwindcss/typography";
Expand Down Expand Up @@ -257,3 +274,4 @@ a {
transform: translateY(0);
}
}
<% } %>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Show } from "solid-js";
<%_ if (routes.some(r => r.url === '/demo/better-auth')) { _%>
import { Link } from "@tanstack/solid-router";
<%_ } _%>
import { authClient } from "../../lib/auth-client";

export default function BetterAuthHeader() {
Expand All @@ -14,6 +16,7 @@ export default function BetterAuthHeader() {
>
<Show
when={session().data?.user}
<%_ if (routes.some(r => r.url === '/demo/better-auth')) { _%>
fallback={
<Link
to="/demo/better-auth"
Expand All @@ -22,6 +25,7 @@ export default function BetterAuthHeader() {
Sign in
</Link>
}
<%_ } _%>
>
{(user) => (
<div class="flex items-center gap-2">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<% if (!includeExamples) { ignoreFile(); return; } %>
import { Link } from '@tanstack/solid-router'
<% for(const integration of integrations.filter(i => i.type === 'header-user')) { %>
import <%= integration.jsName %> from '<%= relativePath(integration.path) %>'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,51 @@ function RootComponent() {
</>
)
}
<% } else if (!includeExamples) { %>
import { HeadContent, Outlet, Scripts, createRootRouteWithContext } from '@tanstack/solid-router'
import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
<% if (addOnEnabled['solid-ui']) { %>
import "@fontsource/inter/400.css"
<% } %>
import { HydrationScript } from 'solid-js/web'
import { Suspense } from 'solid-js'
<% for(const addOn of addOns) {
for(const init of addOn.main?.initialize || []) { %>
<%- init %>
<% } } %>
import styleCss from "../styles.css?url";
Comment on lines +20 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Solid no-examples root branch =="
sed -n '19,63p' packages/create/src/frameworks/solid/project/base/src/routes/__root.tsx.ejs

echo
echo "== Integration definitions that may be rendered here =="
rg -n -C2 --glob '*.ts' --glob '*.js' 'type\s*:\s*["'"'"'](layout|provider|devtools)["'"'"']|integrations\s*:' .

echo
echo "== React root import pattern for comparison =="
sed -n '30,45p' packages/create/src/frameworks/react/project/base/src/routes/__root.tsx.ejs

Repository: TanStack/cli

Length of output: 5568


Add missing import loop for layout integrations in the no-examples branch.

The !includeExamples branch at lines 55–57 renders <%= integration.jsName %> for layout integrations but never imports them. Generated projects with Solid layout integrations will fail to compile with unresolved component references. The React root template correctly handles this pattern by importing integrations before use.

Add an import loop after line 17:

 import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
 
 <% if (addOnEnabled['solid-ui']) { %>
 import "@fontsource/inter/400.css"
 <% } %>
 
+<% for(const integration of integrations.filter(i => i.type === 'layout')) { %>
+import <%= integration.jsName %> from '<%= relativePath(integration.path, true) %>'
+<% } %>
+
 import { HydrationScript } from 'solid-js/web'
📝 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
import { HeadContent, Outlet, Scripts, createRootRouteWithContext } from '@tanstack/solid-router'
import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
<% if (addOnEnabled['solid-ui']) { %>
import "@fontsource/inter/400.css"
<% } %>
import { HydrationScript } from 'solid-js/web'
import { Suspense } from 'solid-js'
<% for(const addOn of addOns) {
for(const init of addOn.main?.initialize || []) { %>
<%- init %>
<% } } %>
import styleCss from "../styles.css?url";
import { HeadContent, Outlet, Scripts, createRootRouteWithContext } from '@tanstack/solid-router'
import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
<% if (addOnEnabled['solid-ui']) { %>
import "@fontsource/inter/400.css"
<% } %>
<% for(const integration of integrations.filter(i => i.type === 'layout')) { %>
import <%= integration.jsName %> from '<%= relativePath(integration.path, true) %>'
<% } %>
import { HydrationScript } from 'solid-js/web'
import { Suspense } from 'solid-js'
<% for(const addOn of addOns) {
for(const init of addOn.main?.initialize || []) { %>
<%- init %>
<% } } %>
import styleCss from "../styles.css?url";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/create/src/frameworks/solid/project/base/src/routes/__root.tsx.ejs`
around lines 20 - 35, The template renders layout integration components (<%=
integration.jsName %>) in the no-examples branch but never imports them; add an
import loop near the top (after existing imports like
createRootRouteWithContext/HydrationScript) that iterates over addOns and their
layout integrations and emits import statements for each integration (using
integration.jsName) so the components referenced in the !includeExamples block
are resolved at compile time.

export const Route = createRootRouteWithContext()({
head: () => ({
links: [{ rel: "stylesheet", href: styleCss }],
}),
shellComponent: RootComponent,
})
function RootComponent() {
return (
<html>
<head>
<HydrationScript />
<HeadContent />
</head>
<body>
<Suspense>
<Outlet />
<TanStackRouterDevtools />
<% for(const integration of integrations.filter(i => i.type === 'layout')) { %>
<<%= integration.jsName %> />
<% } %>
</Suspense>
<Scripts />
</body>
</html>
);
}
<% } else { %>
import { HeadContent, Outlet, Scripts, createRootRouteWithContext } from '@tanstack/solid-router'
import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
Expand Down Expand Up @@ -47,9 +92,9 @@ function RootComponent() {
<html>
<head>
<HydrationScript />
<HeadContent />
</head>
<body>
<HeadContent />
<Suspense>
<Header />
<Outlet />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<% if (!includeExamples) { ignoreFile(); return; } %>
import { createFileRoute } from '@tanstack/solid-router'

export const Route = createFileRoute('/about')({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
<% if (!includeExamples) { %>
import { createFileRoute } from '@tanstack/solid-router'

export const Route = createFileRoute('/')({ component: Home })

function Home() {
return (
<div class="p-8">
<h1 class="text-4xl font-bold">Welcome to TanStack Start</h1>
<p class="mt-4 text-lg">
Edit <code>src/routes/index.tsx</code> to get started.
</p>
</div>
)
}
<% } else { %>
import { createFileRoute } from '@tanstack/solid-router'

export const Route = createFileRoute('/')({ component: App })
Expand Down Expand Up @@ -66,3 +82,4 @@ function App() {
</main>
)
}
<% } %>
Loading
Loading