diff --git a/.changeset/fix-demo-file-leaks.md b/.changeset/fix-demo-file-leaks.md new file mode 100644 index 00000000..b960b9e7 --- /dev/null +++ b/.changeset/fix-demo-file-leaks.md @@ -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. diff --git a/packages/create/src/create-app.ts b/packages/create/src/create-app.ts index eaa93e0f..6551ce8c 100644 --- a/packages/create/src/create-app.ts +++ b/packages/create/src/create-app.ts @@ -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-') ) } @@ -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')), ) + + 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)) }, } }) diff --git a/packages/create/src/frameworks/react/add-ons/better-auth/assets/src/integrations/better-auth/header-user.tsx b/packages/create/src/frameworks/react/add-ons/better-auth/assets/src/integrations/better-auth/header-user.tsx.ejs similarity index 89% rename from packages/create/src/frameworks/react/add-ons/better-auth/assets/src/integrations/better-auth/header-user.tsx rename to packages/create/src/frameworks/react/add-ons/better-auth/assets/src/integrations/better-auth/header-user.tsx.ejs index 6f07c17b..2fd3dca1 100644 --- a/packages/create/src/frameworks/react/add-ons/better-auth/assets/src/integrations/better-auth/header-user.tsx +++ b/packages/create/src/frameworks/react/add-ons/better-auth/assets/src/integrations/better-auth/header-user.tsx.ejs @@ -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(); @@ -34,6 +36,7 @@ export default function BetterAuthHeader() { ); } +<%_ if (routes.some(r => r.url === '/demo/better-auth')) { _%> return ( ); +<%_ } else { _%> + return null; +<%_ } _%> } diff --git a/packages/create/src/frameworks/react/project/base/src/components/Footer.tsx.ejs b/packages/create/src/frameworks/react/project/base/src/components/Footer.tsx.ejs index 6156f908..f70e476b 100644 --- a/packages/create/src/frameworks/react/project/base/src/components/Footer.tsx.ejs +++ b/packages/create/src/frameworks/react/project/base/src/components/Footer.tsx.ejs @@ -1,3 +1,4 @@ +<% if (!includeExamples) { ignoreFile(); return; } %> export default function Footer() { const year = new Date().getFullYear() diff --git a/packages/create/src/frameworks/react/project/base/src/components/Header.tsx.ejs b/packages/create/src/frameworks/react/project/base/src/components/Header.tsx.ejs index f83a81c8..a6f3ed44 100644 --- a/packages/create/src/frameworks/react/project/base/src/components/Header.tsx.ejs +++ b/packages/create/src/frameworks/react/project/base/src/components/Header.tsx.ejs @@ -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' diff --git a/packages/create/src/frameworks/react/project/base/src/components/ThemeToggle.tsx.ejs b/packages/create/src/frameworks/react/project/base/src/components/ThemeToggle.tsx.ejs index 081ebe2b..61b6d82f 100644 --- a/packages/create/src/frameworks/react/project/base/src/components/ThemeToggle.tsx.ejs +++ b/packages/create/src/frameworks/react/project/base/src/components/ThemeToggle.tsx.ejs @@ -1,3 +1,4 @@ +<% if (!includeExamples) { ignoreFile(); return; } %> import { useEffect, useState } from 'react' type ThemeMode = 'light' | 'dark' | 'auto' diff --git a/packages/create/src/frameworks/react/project/base/src/routes/__root.tsx.ejs b/packages/create/src/frameworks/react/project/base/src/routes/__root.tsx.ejs index 22abf72c..2b3098ad 100644 --- a/packages/create/src/frameworks/react/project/base/src/routes/__root.tsx.ejs +++ b/packages/create/src/frameworks/react/project/base/src/routes/__root.tsx.ejs @@ -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 + <% } %> +<% } %> +}<% } %> + +export const Route = <% if (hasContext) { %>createRootRouteWithContext()<% } 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) { %><% } else { %><% } %> + + + + + <% for(const integration of integrations.filter(i => i.type === 'provider')) { %><<%= integration.jsName %>> + <% } %>{children} + , + }, + <% 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()) { %>> + <% } %> + + + ) +} <% } else { %> <% let hasContext = addOnEnabled["apollo-client"] || addOnEnabled["tanstack-query"]; %> import { diff --git a/packages/create/src/frameworks/react/project/base/src/routes/about.tsx.ejs b/packages/create/src/frameworks/react/project/base/src/routes/about.tsx.ejs index 68b82b55..ee76e3fe 100644 --- a/packages/create/src/frameworks/react/project/base/src/routes/about.tsx.ejs +++ b/packages/create/src/frameworks/react/project/base/src/routes/about.tsx.ejs @@ -1,3 +1,4 @@ +<% if (!includeExamples) { ignoreFile(); return; } %> import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/about')({ diff --git a/packages/create/src/frameworks/react/project/base/src/routes/index.tsx.ejs b/packages/create/src/frameworks/react/project/base/src/routes/index.tsx.ejs index 21754f74..7dd740be 100644 --- a/packages/create/src/frameworks/react/project/base/src/routes/index.tsx.ejs +++ b/packages/create/src/frameworks/react/project/base/src/routes/index.tsx.ejs @@ -1,3 +1,19 @@ +<% if (!includeExamples) { %> +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ component: Home }); + +function Home() { + return ( +
+

Welcome to TanStack Start

+

+ Edit src/routes/index.tsx to get started. +

+
+ ); +} +<% } else { %> import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ component: App }); @@ -70,3 +86,4 @@ function App() { ); } +<% } %> diff --git a/packages/create/src/frameworks/react/project/base/src/styles.css.ejs b/packages/create/src/frameworks/react/project/base/src/styles.css.ejs index 498b4c53..559f0e80 100644 --- a/packages/create/src/frameworks/react/project/base/src/styles.css.ejs +++ b/packages/create/src/frameworks/react/project/base/src/styles.css.ejs @@ -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"; @@ -257,3 +274,4 @@ a { transform: translateY(0); } } +<% } %> diff --git a/packages/create/src/frameworks/solid/add-ons/better-auth/assets/src/integrations/better-auth/header-user.tsx b/packages/create/src/frameworks/solid/add-ons/better-auth/assets/src/integrations/better-auth/header-user.tsx.ejs similarity index 92% rename from packages/create/src/frameworks/solid/add-ons/better-auth/assets/src/integrations/better-auth/header-user.tsx rename to packages/create/src/frameworks/solid/add-ons/better-auth/assets/src/integrations/better-auth/header-user.tsx.ejs index a9aee45a..c560fcc6 100644 --- a/packages/create/src/frameworks/solid/add-ons/better-auth/assets/src/integrations/better-auth/header-user.tsx +++ b/packages/create/src/frameworks/solid/add-ons/better-auth/assets/src/integrations/better-auth/header-user.tsx.ejs @@ -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() { @@ -14,6 +16,7 @@ export default function BetterAuthHeader() { > r.url === '/demo/better-auth')) { _%> fallback={ } +<%_ } _%> > {(user) => (
diff --git a/packages/create/src/frameworks/solid/project/base/src/components/Header.tsx.ejs b/packages/create/src/frameworks/solid/project/base/src/components/Header.tsx.ejs index 85ec43d9..e91489d8 100644 --- a/packages/create/src/frameworks/solid/project/base/src/components/Header.tsx.ejs +++ b/packages/create/src/frameworks/solid/project/base/src/components/Header.tsx.ejs @@ -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) %>' diff --git a/packages/create/src/frameworks/solid/project/base/src/routes/__root.tsx.ejs b/packages/create/src/frameworks/solid/project/base/src/routes/__root.tsx.ejs index e8f730c2..949f34df 100644 --- a/packages/create/src/frameworks/solid/project/base/src/routes/__root.tsx.ejs +++ b/packages/create/src/frameworks/solid/project/base/src/routes/__root.tsx.ejs @@ -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"; + +export const Route = createRootRouteWithContext()({ + head: () => ({ + links: [{ rel: "stylesheet", href: styleCss }], + }), + shellComponent: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + <% for(const integration of integrations.filter(i => i.type === 'layout')) { %> + <<%= integration.jsName %> /> + <% } %> + + + + + ); +} <% } else { %> import { HeadContent, Outlet, Scripts, createRootRouteWithContext } from '@tanstack/solid-router' import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' @@ -47,9 +92,9 @@ function RootComponent() { + -
diff --git a/packages/create/src/frameworks/solid/project/base/src/routes/about.tsx.ejs b/packages/create/src/frameworks/solid/project/base/src/routes/about.tsx.ejs index 48d300de..60072ee9 100644 --- a/packages/create/src/frameworks/solid/project/base/src/routes/about.tsx.ejs +++ b/packages/create/src/frameworks/solid/project/base/src/routes/about.tsx.ejs @@ -1,3 +1,4 @@ +<% if (!includeExamples) { ignoreFile(); return; } %> import { createFileRoute } from '@tanstack/solid-router' export const Route = createFileRoute('/about')({ diff --git a/packages/create/src/frameworks/solid/project/base/src/routes/index.tsx.ejs b/packages/create/src/frameworks/solid/project/base/src/routes/index.tsx.ejs index 5d1367b3..dc0e5446 100644 --- a/packages/create/src/frameworks/solid/project/base/src/routes/index.tsx.ejs +++ b/packages/create/src/frameworks/solid/project/base/src/routes/index.tsx.ejs @@ -1,3 +1,19 @@ +<% if (!includeExamples) { %> +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ component: Home }) + +function Home() { + return ( +
+

Welcome to TanStack Start

+

+ Edit src/routes/index.tsx to get started. +

+
+ ) +} +<% } else { %> import { createFileRoute } from '@tanstack/solid-router' export const Route = createFileRoute('/')({ component: App }) @@ -66,3 +82,4 @@ function App() { ) } +<% } %> diff --git a/packages/create/src/frameworks/solid/project/base/src/styles.css.ejs b/packages/create/src/frameworks/solid/project/base/src/styles.css.ejs index a1952e4a..34fd84bb 100644 --- a/packages/create/src/frameworks/solid/project/base/src/styles.css.ejs +++ b/packages/create/src/frameworks/solid/project/base/src/styles.css.ejs @@ -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'; @@ -193,3 +210,4 @@ code { transform: translateY(0); } } +<% } %> diff --git a/packages/create/src/template-file.ts b/packages/create/src/template-file.ts index dfd62236..520a4d23 100644 --- a/packages/create/src/template-file.ts +++ b/packages/create/src/template-file.ts @@ -137,6 +137,7 @@ export function createTemplateFile(environment: Environment, options: Options) { fileRouter: options.mode === 'file-router', codeRouter: options.mode === 'code-router', routerOnly: options.routerOnly === true, + includeExamples: options.includeExamples !== false, addOnEnabled, addOnOption: options.addOnOptions, addOns: options.chosenAddOns, diff --git a/packages/create/tests/create-app.test.ts b/packages/create/tests/create-app.test.ts index 2aef8107..6d77f1c5 100644 --- a/packages/create/tests/create-app.test.ts +++ b/packages/create/tests/create-app.test.ts @@ -117,4 +117,81 @@ describe('createApp', () => { expect(output.files['/src/test2.txt']).toEqual('base64::aGVsbG8=') expect(output.commands.some(({ command }) => command === 'echo')).toBe(true) }) + + it('should strip demo files from add-ons when includeExamples is false', async () => { + const { environment, output } = createMemoryEnvironment() + + const demoFiles = [ + 'src/routes/demo/form.simple.tsx', + 'src/routes/demo.form.tsx', + 'src/routes/example.chat.tsx', + 'src/components/demo-AIAssistant.tsx', + 'src/components/demo.FormComponents.tsx', + 'src/hooks/demo-useAudioRecorder.ts', + 'src/hooks/demo.form.ts', + 'src/lib/demo-store.ts', + 'src/lib/demo.ai-hook.ts', + 'src/data/demo-guitars.ts', + 'src/store/demo.hooks.ts', + 'src/store/demo.store.ts', + 'src/demo.index.css', + 'public/demo-neon.svg', + 'public/example-guitar-flowers.jpg', + ] + const keepFiles = [ + 'src/routes/index.tsx', + 'src/components/Header.tsx', + 'src/lib/utils.ts', + ] + const allFiles = [...demoFiles, ...keepFiles] + + await createApp(environment, { + ...simpleOptions, + includeExamples: false, + chosenAddOns: [ + { + id: 'test-addon', + type: 'add-on', + phase: 'add-on', + packageAdditions: { dependencies: {}, devDependencies: {} }, + routes: [], + integrations: [], + getFiles: () => allFiles, + getFileContents: () => 'content', + getDeletedFiles: () => [], + } as unknown as AddOn, + ], + } as Options) + + for (const file of demoFiles) { + expect(output.files[`/${file}`]).toBeUndefined() + } + for (const file of keepFiles) { + expect(output.files[`/${file}`]).toBeDefined() + } + }) + + it('should keep demo files from add-ons when includeExamples is true', async () => { + const { environment, output } = createMemoryEnvironment() + + await createApp(environment, { + ...simpleOptions, + includeExamples: true, + chosenAddOns: [ + { + id: 'test-addon', + type: 'add-on', + phase: 'add-on', + packageAdditions: { dependencies: {}, devDependencies: {} }, + routes: [], + integrations: [], + getFiles: () => ['src/components/demo-AIAssistant.tsx'], + getFileContents: () => 'content', + getDeletedFiles: () => [], + } as unknown as AddOn, + ], + } as Options) + + expect(output.files['/src/components/demo-AIAssistant.tsx']).toBeDefined() + }) }) diff --git a/packages/create/tests/template-context.test.ts b/packages/create/tests/template-context.test.ts index 6d08c8d8..4c31cff6 100644 --- a/packages/create/tests/template-context.test.ts +++ b/packages/create/tests/template-context.test.ts @@ -311,4 +311,67 @@ export const db = testAddon(/* connection */)` expect(output.files['/test/src/db/__sqlite__index.ts']).toBeUndefined() expect(output.files['/test/src/db/__postgres__index.ts']).toBeUndefined() }) +}) + +describe('Template Context - includeExamples', () => { + it('should default includeExamples to true when undefined', async () => { + const { environment, output } = createMemoryEnvironment() + const templateFile = createTemplateFile(environment, simpleOptions) + environment.startRun() + await templateFile( + 'test.txt.ejs', + '<%= includeExamples ? "with-examples" : "no-examples" %>', + ) + environment.finishRun() + + expect(output.files['/test/test.txt']).toEqual('with-examples') + }) + + it('should set includeExamples to false when options.includeExamples is false', async () => { + const { environment, output } = createMemoryEnvironment() + const templateFile = createTemplateFile(environment, { + ...simpleOptions, + includeExamples: false, + }) + environment.startRun() + await templateFile( + 'test.txt.ejs', + '<%= includeExamples ? "with-examples" : "no-examples" %>', + ) + environment.finishRun() + + expect(output.files['/test/test.txt']).toEqual('no-examples') + }) + + it('should ignore files when includeExamples is false', async () => { + const { environment, output } = createMemoryEnvironment() + const templateFile = createTemplateFile(environment, { + ...simpleOptions, + includeExamples: false, + }) + environment.startRun() + await templateFile( + 'about.ts.ejs', + '<% if (!includeExamples) { ignoreFile(); return; } %>\nexport const about = true', + ) + environment.finishRun() + + expect(output.files['/test/about.ts']).toBeUndefined() + }) + + it('should include files when includeExamples is true', async () => { + const { environment, output } = createMemoryEnvironment() + const templateFile = createTemplateFile(environment, { + ...simpleOptions, + includeExamples: true, + }) + environment.startRun() + await templateFile( + 'about.ts.ejs', + '<% if (!includeExamples) { ignoreFile(); return; } %>\nexport const about = true', + ) + environment.finishRun() + + expect(output.files['/test/about.ts']).toBeDefined() + }) }) \ No newline at end of file