From 0abf74e9d8f5891c8e2b8eb8abd02b100d6fa060 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 7 Jan 2026 13:33:30 -0500 Subject: [PATCH 1/3] Add react router --- src/lib/config.ts | 25 ++ src/lib/constants.ts | 3 + src/nextjs/nextjs-wizard-agent.ts | 5 +- src/react-router/react-router-wizard-agent.ts | 147 +++++++++ src/react-router/utils.ts | 289 ++++++++++++++++++ src/run.ts | 5 + 6 files changed, 471 insertions(+), 3 deletions(-) create mode 100644 src/react-router/react-router-wizard-agent.ts create mode 100644 src/react-router/utils.ts diff --git a/src/lib/config.ts b/src/lib/config.ts index b06d213..1edb2b3 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -110,6 +110,30 @@ export const INTEGRATION_CONFIG = { nextSteps: '• Call posthog.identify() when a user signs into your app\n• Call posthog.capture() to capture custom events in your app\n• Use posthog.isFeatureEnabled() for feature flags', }, + [Integration.reactRouter]: { + name: 'React Router', + filterPatterns: ['**/*.{tsx,ts,jsx,js}'], + ignorePatterns: [ + 'node_modules', + 'dist', + 'build', + 'public', + 'static', + 'assets', + ], + detect: async (options) => { + const packageJson = await getPackageDotJson(options); + return hasPackageInstalled('react-router', packageJson); + }, + generateFilesRules: '', + filterFilesRules: '', + docsUrl: + 'https://posthog-git-react-post-hog.vercel.app/docs/libraries/react-router', + defaultChanges: + '• Installed posthog-js package\n• Added PostHogProvider to the root of the app\n• Integrated PostHog with React Router for pageview tracking', + nextSteps: + '• Call posthog.identify() when a user signs into your app\n• Call posthog.capture() to capture custom events in your app', + }, } as const satisfies Record; export const INTEGRATION_ORDER = [ @@ -117,5 +141,6 @@ export const INTEGRATION_ORDER = [ Integration.astro, Integration.svelte, Integration.reactNative, + Integration.reactRouter, Integration.react, ] as const; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a563dcb..74ea941 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -4,6 +4,7 @@ export enum Integration { svelte = 'svelte', reactNative = 'react-native', astro = 'astro', + reactRouter = 'react-router', } export function getIntegrationDescription(type: string): string { @@ -18,6 +19,8 @@ export function getIntegrationDescription(type: string): string { return 'Svelte'; case Integration.astro: return 'Astro'; + case Integration.reactRouter: + return 'React Router'; default: throw new Error(`Unknown integration ${type}`); } diff --git a/src/nextjs/nextjs-wizard-agent.ts b/src/nextjs/nextjs-wizard-agent.ts index 082c8d2..d147652 100644 --- a/src/nextjs/nextjs-wizard-agent.ts +++ b/src/nextjs/nextjs-wizard-agent.ts @@ -1,6 +1,5 @@ /* Simplified Next.js wizard using posthog-agent with PostHog MCP */ import type { WizardOptions } from '../utils/types'; -import type { FrameworkConfig } from '../lib/framework-config'; import { enableDebugLogs } from '../utils/debug'; import { runAgentWizard } from '../lib/agent-runner'; import { Integration } from '../lib/constants'; @@ -21,7 +20,7 @@ import { */ const MINIMUM_NEXTJS_VERSION = '15.3.0'; -const NEXTJS_AGENT_CONFIG: FrameworkConfig = { +const NEXTJS_AGENT_CONFIG = { metadata: { name: 'Next.js', integration: Integration.nextjs, @@ -44,7 +43,7 @@ const NEXTJS_AGENT_CONFIG: FrameworkConfig = { environment: { uploadToHosting: true, - getEnvVars: (apiKey, host) => ({ + getEnvVars: (apiKey: string, host: string) => ({ NEXT_PUBLIC_POSTHOG_KEY: apiKey, NEXT_PUBLIC_POSTHOG_HOST: host, }), diff --git a/src/react-router/react-router-wizard-agent.ts b/src/react-router/react-router-wizard-agent.ts new file mode 100644 index 0000000..c3084c6 --- /dev/null +++ b/src/react-router/react-router-wizard-agent.ts @@ -0,0 +1,147 @@ +/* React Router wizard using posthog-agent with PostHog MCP */ +import type { WizardOptions } from '../utils/types'; +import type { FrameworkConfig } from '../lib/framework-config'; +import { enableDebugLogs } from '../utils/debug'; +import { runAgentWizard } from '../lib/agent-runner'; +import { Integration } from '../lib/constants'; +import { getPackageVersion } from '../utils/package-json'; +import { getPackageDotJson } from '../utils/clack-utils'; +import clack from '../utils/clack'; +import chalk from 'chalk'; +import * as semver from 'semver'; +import { + getReactRouterMode, + getReactRouterModeName, + getReactRouterVersionBucket, + ReactRouterMode, +} from './utils'; + +/** + * React Router framework configuration for the universal agent runner. + */ +const MINIMUM_REACT_ROUTER_VERSION = '6.0.0'; + +const REACT_ROUTER_AGENT_CONFIG: FrameworkConfig = { + metadata: { + name: 'React Router', + integration: Integration.reactRouter, + docsUrl: 'https://posthog.com/docs/libraries/react', + unsupportedVersionDocsUrl: 'https://posthog.com/docs/libraries/react', + abortMessage: + 'This wizard uses an LLM agent to intelligently modify your project. Please view the docs to setup React Router manually instead: https://posthog.com/docs/libraries/react', + gatherContext: async (options: WizardOptions) => { + const routerMode = await getReactRouterMode(options); + return { routerMode }; + }, + }, + + detection: { + packageName: 'react-router', + packageDisplayName: 'React Router', + getVersion: (packageJson: any) => + getPackageVersion('react-router', packageJson), + getVersionBucket: getReactRouterVersionBucket, + }, + + environment: { + uploadToHosting: false, + getEnvVars: (apiKey: string, host: string) => ({ + REACT_APP_POSTHOG_KEY: apiKey, + REACT_APP_POSTHOG_HOST: host, + }), + }, + + analytics: { + getTags: (context: any) => { + const routerMode = context.routerMode as ReactRouterMode; + return { + routerMode: routerMode || 'unknown', + }; + }, + }, + + prompts: { + getAdditionalContextLines: (context: any) => { + const routerMode = context.routerMode as ReactRouterMode; + const modeName = routerMode + ? getReactRouterModeName(routerMode) + : 'unknown'; + + // Map router mode to framework ID for MCP docs resource + const frameworkIdMap: Record = { + [ReactRouterMode.V6]: 'react-react-router-v6', + [ReactRouterMode.V7_FRAMEWORK]: 'react-react-router-v7-framework', + [ReactRouterMode.V7_DATA]: 'react-react-router-v7-data', + [ReactRouterMode.V7_DECLARATIVE]: 'react-react-router-v7-declarative', + }; + + const frameworkId = routerMode + ? frameworkIdMap[routerMode] + : ReactRouterMode.V7_FRAMEWORK; + + return [ + `Router mode: ${modeName}`, + `Framework docs ID: ${frameworkId} (use posthog://docs/frameworks/${frameworkId} for documentation)`, + ]; + }, + }, + + ui: { + welcomeMessage: 'PostHog React Router wizard (agent-powered)', + spinnerMessage: + 'Writing your PostHog setup with events, error capture and more...', + successMessage: 'PostHog integration complete', + estimatedDurationMinutes: 8, + getOutroChanges: (context: any) => { + const routerMode = context.routerMode as ReactRouterMode; + const modeName = routerMode + ? getReactRouterModeName(routerMode) + : 'React Router'; + return [ + `Analyzed your React Router project structure (${modeName})`, + `Created and configured PostHog initializers`, + `Integrated PostHog into your application`, + ]; + }, + getOutroNextSteps: () => [ + 'Start your development server to see PostHog in action', + 'Visit your PostHog dashboard to see incoming events', + ], + }, +}; + +/** + * React Router wizard powered by the universal agent runner. + */ +export async function runReactRouterWizardAgent( + options: WizardOptions, +): Promise { + if (options.debug) { + enableDebugLogs(); + } + + // Check React Router version - agent wizard requires >= 6.0.0 + const packageJson = await getPackageDotJson(options); + const reactRouterVersion = getPackageVersion('react-router', packageJson); + + if (reactRouterVersion) { + const coercedVersion = semver.coerce(reactRouterVersion); + if ( + coercedVersion && + semver.lt(coercedVersion, MINIMUM_REACT_ROUTER_VERSION) + ) { + const docsUrl = + REACT_ROUTER_AGENT_CONFIG.metadata.unsupportedVersionDocsUrl ?? + REACT_ROUTER_AGENT_CONFIG.metadata.docsUrl; + + clack.log.warn( + `Sorry: the wizard can't help you with React Router ${reactRouterVersion}. Upgrade to React Router ${MINIMUM_REACT_ROUTER_VERSION} or later, or check out the manual setup guide.`, + ); + clack.log.info(`Setup React Router manually: ${chalk.cyan(docsUrl)}`); + clack.outro('PostHog wizard will see you next time!'); + return; + } + } + + await runAgentWizard(REACT_ROUTER_AGENT_CONFIG, options); +} diff --git a/src/react-router/utils.ts b/src/react-router/utils.ts new file mode 100644 index 0000000..3002430 --- /dev/null +++ b/src/react-router/utils.ts @@ -0,0 +1,289 @@ +import { major, minVersion } from 'semver'; +import fg from 'fast-glob'; +import { abortIfCancelled, getPackageDotJson } from '../utils/clack-utils'; +import clack from '../utils/clack'; +import type { WizardOptions } from '../utils/types'; +import { Integration } from '../lib/constants'; +import { getPackageVersion } from '../utils/package-json'; +import chalk from 'chalk'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as semver from 'semver'; + +export enum ReactRouterMode { + V6 = 'v6', // React Router v6 + V7_FRAMEWORK = 'v7-framework', // React Router v7 with react-router.config.ts + V7_DATA = 'v7-data', // React Router v7 with createBrowserRouter + V7_DECLARATIVE = 'v7-declarative', // React Router v7 with BrowserRouter +} + +const IGNORE_PATTERNS = [ + '**/node_modules/**', + '**/dist/**', + '**/build/**', + '**/public/**', + '**/.next/**', +]; + +/** + * Get React Router version bucket for analytics + */ +export function getReactRouterVersionBucket( + version: string | undefined, +): string { + if (!version) { + return 'none'; + } + + try { + const minVer = minVersion(version); + if (!minVer) { + return 'invalid'; + } + const majorVersion = major(minVer); + if (majorVersion >= 6) { + return `${majorVersion}.x`; + } + return `<6.0.0`; + } catch { + return 'unknown'; + } +} + +/** + * Check if react-router.config.ts exists (indicates framework mode - React Router v7) + */ +async function hasReactRouterConfig({ + installDir, +}: Pick): Promise { + const configMatches = await fg('**/react-router.config.@(ts|js|tsx|jsx)', { + dot: true, + cwd: installDir, + ignore: IGNORE_PATTERNS, + }); + + return configMatches.length > 0; +} + +/** + * Search for createBrowserRouter usage in source files + */ +async function hasCreateBrowserRouter({ + installDir, +}: Pick): Promise { + const sourceFiles = await fg('**/*.@(ts|tsx|js|jsx)', { + dot: true, + cwd: installDir, + ignore: IGNORE_PATTERNS, + }); + + for (const file of sourceFiles) { + try { + const filePath = path.join(installDir, file); + const content = fs.readFileSync(filePath, 'utf-8'); + + // Check for createBrowserRouter import or usage + if (content.includes('createBrowserRouter')) { + return true; + } + } catch { + // Skip files that can't be read + continue; + } + } + + return false; +} + +/** + * Search for declarative BrowserRouter usage + */ +async function hasDeclarativeRouter({ + installDir, +}: Pick): Promise { + const sourceFiles = await fg('**/*.@(ts|tsx|js|jsx)', { + dot: true, + cwd: installDir, + ignore: IGNORE_PATTERNS, + }); + + for (const file of sourceFiles) { + try { + const filePath = path.join(installDir, file); + const content = fs.readFileSync(filePath, 'utf-8'); + + // Check for BrowserRouter usage (JSX or import) + if ( + content.includes(' { + const { installDir } = options; + + // First, get the React Router version + const packageJson = await getPackageDotJson(options); + const reactRouterVersion = + getPackageVersion('react-router-dom', packageJson) || + getPackageVersion('react-router', packageJson); + + if (!reactRouterVersion) { + // If we can't detect version, ask the user + clack.log.info( + `Learn more about React Router modes: ${chalk.cyan( + 'https://reactrouter.com/start/modes', + )}`, + ); + const result: ReactRouterMode = await abortIfCancelled( + clack.select({ + message: 'What React Router version and mode are you using?', + options: [ + { + label: 'React Router v6', + value: ReactRouterMode.V6, + }, + { + label: 'React Router v7 - Framework mode', + value: ReactRouterMode.V7_FRAMEWORK, + }, + { + label: 'React Router v7 - Data mode', + value: ReactRouterMode.V7_DATA, + }, + { + label: 'React Router v7 - Declarative mode', + value: ReactRouterMode.V7_DECLARATIVE, + }, + ], + }), + Integration.reactRouter, + ); + return result; + } + + const coercedVersion = semver.coerce(reactRouterVersion); + const majorVersion = coercedVersion ? major(coercedVersion) : null; + + // If v6, return V6 + if (majorVersion === 6) { + clack.log.info('Detected React Router v6'); + return ReactRouterMode.V6; + } + + // If v7, detect the mode + if (majorVersion === 7) { + // First check for framework mode (react-router.config.ts) + const hasConfig = await hasReactRouterConfig({ installDir }); + if (hasConfig) { + clack.log.info('Detected React Router v7 - Framework mode'); + return ReactRouterMode.V7_FRAMEWORK; + } + + // Check for data mode (createBrowserRouter) + const hasDataMode = await hasCreateBrowserRouter({ installDir }); + if (hasDataMode) { + clack.log.info('Detected React Router v7 - Data mode'); + return ReactRouterMode.V7_DATA; + } + + // Check for declarative mode (BrowserRouter) + const hasDeclarative = await hasDeclarativeRouter({ installDir }); + if (hasDeclarative) { + clack.log.info('Detected React Router v7 - Declarative mode'); + return ReactRouterMode.V7_DECLARATIVE; + } + + // If v7 but can't detect mode, ask the user + clack.log.info( + `Learn more about React Router modes: ${chalk.cyan( + 'https://reactrouter.com/start/modes', + )}`, + ); + const result: ReactRouterMode = await abortIfCancelled( + clack.select({ + message: 'What React Router v7 mode are you using?', + options: [ + { + label: 'Framework mode', + value: ReactRouterMode.V7_FRAMEWORK, + }, + { + label: 'Data mode', + value: ReactRouterMode.V7_DATA, + }, + { + label: 'Declarative mode', + value: ReactRouterMode.V7_DECLARATIVE, + }, + ], + }), + Integration.reactRouter, + ); + return result; + } + + // If version is not 6 or 7, default to asking + clack.log.info( + `Learn more about React Router modes: ${chalk.cyan( + 'https://reactrouter.com/start/modes', + )}`, + ); + const result: ReactRouterMode = await abortIfCancelled( + clack.select({ + message: 'What React Router version and mode are you using?', + options: [ + { + label: 'React Router v6', + value: ReactRouterMode.V6, + }, + { + label: 'React Router v7 - Framework mode', + value: ReactRouterMode.V7_FRAMEWORK, + }, + { + label: 'React Router v7 - Data mode', + value: ReactRouterMode.V7_DATA, + }, + { + label: 'React Router v7 - Declarative mode', + value: ReactRouterMode.V7_DECLARATIVE, + }, + ], + }), + Integration.reactRouter, + ); + return result; +} + +/** + * Get human-readable name for React Router mode + */ +export function getReactRouterModeName(mode: ReactRouterMode): string { + switch (mode) { + case ReactRouterMode.V6: + return 'v6'; + case ReactRouterMode.V7_FRAMEWORK: + return 'v7 Framework mode'; + case ReactRouterMode.V7_DATA: + return 'v7 Data mode'; + case ReactRouterMode.V7_DECLARATIVE: + return 'v7 Declarative mode'; + } +} diff --git a/src/run.ts b/src/run.ts index 2a9faf0..7a9dada 100644 --- a/src/run.ts +++ b/src/run.ts @@ -13,6 +13,7 @@ import { analytics } from './utils/analytics'; import { runSvelteWizard } from './svelte/svelte-wizard'; import { runReactNativeWizard } from './react-native/react-native-wizard'; import { runAstroWizard } from './astro/astro-wizard'; +import { runReactRouterWizardAgent } from './react-router/react-router-wizard-agent'; import { EventEmitter } from 'events'; import chalk from 'chalk'; import { RateLimitError } from './utils/errors'; @@ -81,6 +82,9 @@ export async function runWizard(argv: Args) { case Integration.astro: await runAstroWizard(wizardOptions); break; + case Integration.reactRouter: + await runReactRouterWizardAgent(wizardOptions); + break; default: clack.log.error('No setup wizard selected!'); } @@ -141,6 +145,7 @@ async function getIntegrationForSetup( { value: Integration.nextjs, label: 'Next.js' }, { value: Integration.astro, label: 'Astro' }, { value: Integration.react, label: 'React' }, + { value: Integration.reactRouter, label: 'React Router' }, { value: Integration.svelte, label: 'Svelte' }, { value: Integration.reactNative, label: 'React Native' }, ], From dfc1f720f2356af3ceb2970a0563487698504cad Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 7 Jan 2026 20:06:52 -0500 Subject: [PATCH 2/3] Add feature flag --- src/lib/constants.ts | 4 ++++ src/run.ts | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 74ea941..5e9d36e 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -7,6 +7,10 @@ export enum Integration { reactRouter = 'react-router', } +export enum FeatureFlagDefinition { + ReactRouter = 'wizard-react-router', +} + export function getIntegrationDescription(type: string): string { switch (type) { case Integration.nextjs: diff --git a/src/run.ts b/src/run.ts index 7a9dada..8df8eef 100644 --- a/src/run.ts +++ b/src/run.ts @@ -3,7 +3,11 @@ import { abortIfCancelled } from './utils/clack-utils'; import { runNextjsWizardAgent } from './nextjs/nextjs-wizard-agent'; import type { CloudRegion, WizardOptions } from './utils/types'; -import { getIntegrationDescription, Integration } from './lib/constants'; +import { + getIntegrationDescription, + Integration, + FeatureFlagDefinition, +} from './lib/constants'; import { readEnvironment } from './utils/environment'; import clack from './utils/clack'; import path from 'path'; @@ -82,9 +86,17 @@ export async function runWizard(argv: Args) { case Integration.astro: await runAstroWizard(wizardOptions); break; - case Integration.reactRouter: - await runReactRouterWizardAgent(wizardOptions); + case Integration.reactRouter: { + const flagValue = await analytics.getFeatureFlag( + FeatureFlagDefinition.ReactRouter, + ); + if (flagValue === true) { + await runReactRouterWizardAgent(wizardOptions); + } else { + await runReactWizard(wizardOptions); + } break; + } default: clack.log.error('No setup wizard selected!'); } From 8158c727680e4652cbf22813a34bafebdb902679 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 8 Jan 2026 15:00:30 -0500 Subject: [PATCH 3/3] fix names --- src/react-router/react-router-wizard-agent.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/react-router/react-router-wizard-agent.ts b/src/react-router/react-router-wizard-agent.ts index c3084c6..a23aadf 100644 --- a/src/react-router/react-router-wizard-agent.ts +++ b/src/react-router/react-router-wizard-agent.ts @@ -69,10 +69,10 @@ const REACT_ROUTER_AGENT_CONFIG: FrameworkConfig = { // Map router mode to framework ID for MCP docs resource const frameworkIdMap: Record = { - [ReactRouterMode.V6]: 'react-react-router-v6', - [ReactRouterMode.V7_FRAMEWORK]: 'react-react-router-v7-framework', - [ReactRouterMode.V7_DATA]: 'react-react-router-v7-data', - [ReactRouterMode.V7_DECLARATIVE]: 'react-react-router-v7-declarative', + [ReactRouterMode.V6]: 'react-react-router-6', + [ReactRouterMode.V7_FRAMEWORK]: 'react-react-router-7-framework', + [ReactRouterMode.V7_DATA]: 'react-react-router-7-data', + [ReactRouterMode.V7_DECLARATIVE]: 'react-react-router-7-declarative', }; const frameworkId = routerMode