diff --git a/e2e-tests/test-applications/tanstack-start-test-app/.gitignore b/e2e-tests/test-applications/tanstack-start-test-app/.gitignore new file mode 100644 index 000000000..b2bb8c70b --- /dev/null +++ b/e2e-tests/test-applications/tanstack-start-test-app/.gitignore @@ -0,0 +1,6 @@ +node_modules +.DS_Store +.env +.output +.nitro +.tanstack diff --git a/e2e-tests/test-applications/tanstack-start-test-app/package.json b/e2e-tests/test-applications/tanstack-start-test-app/package.json new file mode 100644 index 000000000..562163ff9 --- /dev/null +++ b/e2e-tests/test-applications/tanstack-start-test-app/package.json @@ -0,0 +1,26 @@ +{ + "name": "tanstack-start-test-app", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "start": "node .output/server/index.mjs" + }, + "dependencies": { + "@tanstack/react-router": "^1.132.0", + "@tanstack/react-start": "^1.132.0", + "@tanstack/router-plugin": "^1.132.0", + "nitro": "latest", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "vite-tsconfig-paths": "^6.0.2" + }, + "devDependencies": { + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.4", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} diff --git a/e2e-tests/test-applications/tanstack-start-test-app/src/routeTree.gen.ts b/e2e-tests/test-applications/tanstack-start-test-app/src/routeTree.gen.ts new file mode 100644 index 000000000..dceedffdc --- /dev/null +++ b/e2e-tests/test-applications/tanstack-start-test-app/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* 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 IndexRouteImport } from './routes/index' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e-tests/test-applications/tanstack-start-test-app/src/router.tsx b/e2e-tests/test-applications/tanstack-start-test-app/src/router.tsx new file mode 100644 index 000000000..29472c661 --- /dev/null +++ b/e2e-tests/test-applications/tanstack-start-test-app/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router'; +import { routeTree } from '@/routeTree.gen'; + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, + }); +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType; + } +} diff --git a/e2e-tests/test-applications/tanstack-start-test-app/src/routes/__root.tsx b/e2e-tests/test-applications/tanstack-start-test-app/src/routes/__root.tsx new file mode 100644 index 000000000..cf42795d1 --- /dev/null +++ b/e2e-tests/test-applications/tanstack-start-test-app/src/routes/__root.tsx @@ -0,0 +1,30 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router'; + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + ], + }), + component: RootLayout, +}); + +function RootLayout() { + return ( + + + + + + + + + + ); +} diff --git a/e2e-tests/test-applications/tanstack-start-test-app/src/routes/index.tsx b/e2e-tests/test-applications/tanstack-start-test-app/src/routes/index.tsx new file mode 100644 index 000000000..8dab59a6f --- /dev/null +++ b/e2e-tests/test-applications/tanstack-start-test-app/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/')({ + component: Home, +}); + +function Home() { + return

Home

; +} diff --git a/e2e-tests/test-applications/tanstack-start-test-app/tsconfig.json b/e2e-tests/test-applications/tanstack-start-test-app/tsconfig.json new file mode 100644 index 000000000..7d15a8774 --- /dev/null +++ b/e2e-tests/test-applications/tanstack-start-test-app/tsconfig.json @@ -0,0 +1,20 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "target": "ES2022", + "jsx": "react-jsx", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": false, + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/e2e-tests/test-applications/tanstack-start-test-app/vite.config.ts b/e2e-tests/test-applications/tanstack-start-test-app/vite.config.ts new file mode 100644 index 000000000..973ce87ca --- /dev/null +++ b/e2e-tests/test-applications/tanstack-start-test-app/vite.config.ts @@ -0,0 +1,14 @@ +import { tanstackStart } from '@tanstack/react-start/plugin/vite'; +import { defineConfig } from 'vite'; +import tsConfigPaths from 'vite-tsconfig-paths'; +import viteReact from '@vitejs/plugin-react'; +import { nitro } from 'nitro/vite'; + +export default defineConfig({ + plugins: [ + tsConfigPaths({ projects: ['./tsconfig.json'] }), + tanstackStart({ srcDirectory: 'src' }), + viteReact(), + nitro(), + ], +}); diff --git a/e2e-tests/tests/help-message.test.ts b/e2e-tests/tests/help-message.test.ts index 9fc84bffc..755277ebe 100644 --- a/e2e-tests/tests/help-message.test.ts +++ b/e2e-tests/tests/help-message.test.ts @@ -31,7 +31,7 @@ describe('--help command', () => { env: SENTRY_WIZARD_INTEGRATION [choices: "reactNative", "flutter", "ios", "android", "cordova", "angular", "cloudflare", "electron", "nextjs", "nuxt", "remix", "reactRouter", - "sveltekit", "sourcemaps"] + "sveltekit", "tanstackStart", "sourcemaps"] -p, --platform Choose platform(s) env: SENTRY_WIZARD_PLATFORM [array] [choices: "ios", "android"] diff --git a/e2e-tests/tests/tanstack-start.test.ts b/e2e-tests/tests/tanstack-start.test.ts new file mode 100644 index 000000000..15b24f6cb --- /dev/null +++ b/e2e-tests/tests/tanstack-start.test.ts @@ -0,0 +1,57 @@ +import { Integration } from '../../lib/Constants'; +import { + checkIfBuilds, + checkIfRunsOnDevMode, + checkIfRunsOnProdMode, + checkPackageJson, + createIsolatedTestEnv, + getWizardCommand, +} from '../utils'; +import { afterAll, beforeAll, describe, expect, test } from 'vitest'; + +//@ts-expect-error - clifty is ESM only +import { KEYS, withEnv } from 'clifty'; + +describe('TanStack Start', () => { + let wizardExitCode: number; + const { projectDir, cleanup } = createIsolatedTestEnv( + 'tanstack-start-test-app', + ); + + beforeAll(async () => { + wizardExitCode = await withEnv({ cwd: projectDir }) + .defineInteraction() + .whenAsked('Please select your package manager.') + .respondWith(KEYS.DOWN, KEYS.ENTER) + .expectOutput('Installing @sentry/tanstackstart-react') + .expectOutput('Installed @sentry/tanstackstart-react', { + timeout: 240_000, + }) + .expectOutput('Successfully installed the Sentry TanStack Start SDK!') + .run(getWizardCommand(Integration.tanstackStart)); + }); + + afterAll(() => { + cleanup(); + }); + + test('exits with exit code 0', () => { + expect(wizardExitCode).toBe(0); + }); + + test('package.json is updated correctly', () => { + checkPackageJson(projectDir, '@sentry/tanstackstart-react'); + }); + + test('builds successfully', async () => { + await checkIfBuilds(projectDir); + }); + + test('runs on dev mode correctly', async () => { + await checkIfRunsOnDevMode(projectDir, 'ready in'); + }); + + test('runs on prod mode correctly', async () => { + await checkIfRunsOnProdMode(projectDir, 'Listening on'); + }); +}); diff --git a/lib/Constants.ts b/lib/Constants.ts index 92cab6c4a..e70538239 100644 --- a/lib/Constants.ts +++ b/lib/Constants.ts @@ -13,6 +13,7 @@ export enum Integration { remix = 'remix', reactRouter = 'reactRouter', sveltekit = 'sveltekit', + tanstackStart = 'tanstackStart', sourcemaps = 'sourcemaps', } @@ -69,6 +70,8 @@ export function getIntegrationDescription(type: string): string { return 'iOS'; case Integration.cloudflare: return 'Cloudflare'; + case Integration.tanstackStart: + return 'TanStack Start'; default: return 'React Native'; } @@ -100,6 +103,8 @@ export function mapIntegrationToPlatform(type: string): string | undefined { return undefined; case Integration.cloudflare: return 'node-cloudflare-workers'; + case Integration.tanstackStart: + return 'javascript-tanstack-start'; case Integration.ios: return 'iOS'; default: diff --git a/src/run.ts b/src/run.ts index c3304b08e..a5a7e906c 100644 --- a/src/run.ts +++ b/src/run.ts @@ -17,6 +17,7 @@ import { runSourcemapsWizard } from './sourcemaps/sourcemaps-wizard'; import { runSvelteKitWizard } from './sveltekit/sveltekit-wizard'; import { runReactRouterWizard } from './react-router/react-router-wizard'; import { runCloudflareWizard } from './cloudflare/cloudflare-wizard'; +import { runTanstackStartWizard } from './tanstack-start/tanstack-start-wizard'; import { enableDebugLogs } from './utils/debug'; import type { PreselectedProject, WizardOptions } from './utils/types'; import { WIZARD_VERSION } from './version'; @@ -35,6 +36,7 @@ type WizardIntegration = | 'reactRouter' | 'sveltekit' | 'cloudflare' + | 'tanstackStart' | 'sourcemaps'; type Args = { @@ -131,6 +133,7 @@ export async function run(argv: Args) { { value: 'reactRouter', label: 'React Router' }, { value: 'sveltekit', label: 'SvelteKit' }, { value: 'cloudflare', label: 'Cloudflare' }, + { value: 'tanstackStart', label: 'TanStack Start' }, { value: 'sourcemaps', label: 'Configure Source Maps Upload' }, ], }), @@ -206,6 +209,10 @@ export async function run(argv: Args) { await runCloudflareWizard(wizardOptions); break; + case 'tanstackStart': + await runTanstackStartWizard(wizardOptions); + break; + case 'sourcemaps': await runSourcemapsWizard(wizardOptions); break; diff --git a/src/tanstack-start/tanstack-start-wizard.ts b/src/tanstack-start/tanstack-start-wizard.ts new file mode 100644 index 000000000..a69f7ab24 --- /dev/null +++ b/src/tanstack-start/tanstack-start-wizard.ts @@ -0,0 +1,85 @@ +// @ts-expect-error - clack is ESM and TS complains about that. It works though +import clack from '@clack/prompts'; +import chalk from 'chalk'; + +import type { WizardOptions } from '../utils/types'; +import { withTelemetry } from '../telemetry'; +import { + abort, + confirmContinueIfNoOrDirtyGitRepo, + getOrAskForProjectData, + getPackageDotJson, + printWelcome, + installPackage, +} from '../utils/clack'; +import { hasPackageInstalled } from '../utils/package-json'; +import { abortIfSpotlightNotSupported } from '../utils/abort-if-sportlight-not-supported'; + +export async function runTanstackStartWizard( + options: WizardOptions, +): Promise { + return withTelemetry( + { + enabled: options.telemetryEnabled, + integration: 'tanstackStart', + wizardOptions: options, + }, + () => runTanstackStartWizardWithTelemetry(options), + ); +} + +async function runTanstackStartWizardWithTelemetry( + options: WizardOptions, +): Promise { + const { promoCode, ignoreGitChanges, forceInstall } = options; + + printWelcome({ + wizardName: 'Sentry TanStack Start Wizard', + promoCode, + }); + + const packageJson = await getPackageDotJson(); + + if (!packageJson) { + clack.log.error( + 'Could not find a package.json file in the current directory', + ); + return; + } + + if (!hasPackageInstalled('@tanstack/react-start', packageJson)) { + await abort( + 'This wizard requires a TanStack Start project. Please make sure you have @tanstack/react-start installed.', + ); + return; + } + + await confirmContinueIfNoOrDirtyGitRepo({ + ignoreGitChanges, + cwd: undefined, + }); + + const sentryAlreadyInstalled = hasPackageInstalled( + '@sentry/tanstackstart-react', + packageJson, + ); + + const projectData = await getOrAskForProjectData( + options, + 'javascript-tanstack-start', + ); + + if (projectData.spotlight) { + return abortIfSpotlightNotSupported('TanStack Start'); + } + + await installPackage({ + packageName: '@sentry/tanstackstart-react', + alreadyInstalled: sentryAlreadyInstalled, + forceInstall, + }); + + clack.outro( + `${chalk.green('Successfully installed the Sentry TanStack Start SDK!')}`, + ); +} diff --git a/src/utils/clack/index.ts b/src/utils/clack/index.ts index 122112ab6..35f0bc205 100644 --- a/src/utils/clack/index.ts +++ b/src/utils/clack/index.ts @@ -1188,6 +1188,7 @@ export async function getOrAskForProjectData( | 'javascript-react-router' | 'javascript-remix' | 'javascript-sveltekit' + | 'javascript-tanstack-start' | 'node-cloudflare-workers' | 'apple-ios' | 'android' @@ -1376,6 +1377,7 @@ export async function askForWizardLogin(options: { | 'javascript-react-router' | 'javascript-remix' | 'javascript-sveltekit' + | 'javascript-tanstack-start' | 'node-cloudflare-workers' | 'apple-ios' | 'android'