From e3d6e4dfb83cf4364604e1cb4061384c897b945d Mon Sep 17 00:00:00 2001 From: Mohamed Khaled Date: Tue, 28 Jul 2026 16:47:50 +0300 Subject: [PATCH] feat(detection): add alternatePackageNames so React Router v6 is detected React Router v6 ships the router as `react-router-dom`; v7 consolidated onto `react-router`. The detection gate only checked `react-router`, so v6 projects declared only as `react-router-dom` never matched and fell through to the javascript_web fallback. The rest of the config already assumes v6 works: `minimumVersion` is '6.0.0' and `getReactRouterMode` has an explicit V6 branch, but neither was reachable because detection ran first. context-mill's dedicated `react-react-router-6` skill variant was unreachable for the same reason. Adds an optional `alternatePackageNames` to FrameworkDetection rather than special-casing react-router in the runner, and uses both names in detect, getVersion, getInstalledVersion, and the is-it-installed warning. Verified against context-mill's example apps by running every framework predicate over each one: react-react-router-6 resolved to javascript_web before and react-router after, with the three v7 variants unchanged. --- .../react-router/react-router-wizard-agent.ts | 31 +++++++++++++++---- src/lib/detection/__tests__/framework.test.ts | 31 +++++++++++++++++++ src/lib/framework-config.ts | 8 +++++ src/lib/programs/posthog-integration/index.ts | 8 +++-- 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/frameworks/react-router/react-router-wizard-agent.ts b/src/frameworks/react-router/react-router-wizard-agent.ts index e1c211ebe..2acba07b9 100644 --- a/src/frameworks/react-router/react-router-wizard-agent.ts +++ b/src/frameworks/react-router/react-router-wizard-agent.ts @@ -4,9 +4,8 @@ import type { FrameworkConfig } from '@lib/framework-config'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; import { Integration } from '@lib/constants'; import { - getDeclaredVersion, + findDeclaredPackage, getInstalledPackageVersion, - hasDeclaredDependency, type PackageJson, } from '@utils/package-json'; import { tryGetPackageJson } from '@utils/setup-utils'; @@ -22,6 +21,22 @@ type ReactRouterContext = { routerMode?: ReactRouterMode; }; +const REACT_ROUTER_PACKAGE = 'react-router'; + +/** + * v6 ships the router as `react-router-dom`; v7 consolidated onto + * `react-router`. Both names have to count, or v6 projects — which declare + * only `react-router-dom` — never match and fall through to the generic + * JavaScript fallbacks. + */ +const REACT_ROUTER_ALTERNATE_PACKAGES = ['react-router-dom']; + +/** Lookup order matches `getReactRouterMode` in ./utils. */ +const REACT_ROUTER_PACKAGES = [ + ...REACT_ROUTER_ALTERNATE_PACKAGES, + REACT_ROUTER_PACKAGE, +]; + export const REACT_ROUTER_AGENT_CONFIG: FrameworkConfig = { metadata: { name: 'React Router', @@ -41,20 +56,24 @@ export const REACT_ROUTER_AGENT_CONFIG: FrameworkConfig = { }, detection: { - packageName: 'react-router', + packageName: REACT_ROUTER_PACKAGE, + alternatePackageNames: REACT_ROUTER_ALTERNATE_PACKAGES, packageDisplayName: 'React Router', getVersion: (packageJson: unknown) => - getDeclaredVersion('react-router', packageJson as PackageJson), + findDeclaredPackage(REACT_ROUTER_PACKAGES, packageJson as PackageJson) + ?.version, getVersionBucket: getReactRouterVersionBucket, minimumVersion: '6.0.0', getInstalledVersion: (options: WizardRunOptions) => Promise.resolve( - getInstalledPackageVersion('react-router', options.installDir), + REACT_ROUTER_PACKAGES.map((name) => + getInstalledPackageVersion(name, options.installDir), + ).find((version) => version !== undefined), ), detect: async (options) => { const packageJson = await tryGetPackageJson(options); return packageJson - ? hasDeclaredDependency('react-router', packageJson) + ? findDeclaredPackage(REACT_ROUTER_PACKAGES, packageJson) !== undefined : false; }, detectPackageManager: detectNodePackageManagers, diff --git a/src/lib/detection/__tests__/framework.test.ts b/src/lib/detection/__tests__/framework.test.ts index 075c46a29..e6959a048 100644 --- a/src/lib/detection/__tests__/framework.test.ts +++ b/src/lib/detection/__tests__/framework.test.ts @@ -79,6 +79,37 @@ describe('detectFramework (end-to-end over real project dirs)', () => { ); }); + test('a React Router v6 app resolves to react-router, not a JS fallback', async () => { + // v6 declares only `react-router-dom`; v7 consolidated onto `react-router`. + // Matching just the v7 name sent every v6 project to javascript_web. + const opts = project({ + 'package.json': JSON.stringify({ + dependencies: { + react: '^18', + 'react-dom': '^18', + 'react-router-dom': '^6.30.2', + }, + }), + 'package-lock.json': '{}', + 'index.html': '', + }); + await expect(detectFramework(opts.installDir)).resolves.toBe( + Integration.reactRouter, + ); + }); + + test('a React Router v7 app resolves to react-router', async () => { + const opts = project({ + 'package.json': JSON.stringify({ + dependencies: { react: '^19', 'react-router': '7.10.1' }, + }), + 'package-lock.json': '{}', + }); + await expect(detectFramework(opts.installDir)).resolves.toBe( + Integration.reactRouter, + ); + }); + test('a Vite React app resolves to javascript_web, not javascript_node', async () => { const opts = project({ 'package.json': JSON.stringify({ diff --git a/src/lib/framework-config.ts b/src/lib/framework-config.ts index 5bbc7440b..e2387d617 100644 --- a/src/lib/framework-config.ts +++ b/src/lib/framework-config.ts @@ -92,6 +92,14 @@ export interface FrameworkDetection { /** Package name to check in package.json (e.g., "next", "react") */ packageName: string; + /** + * Other package names that count as this framework being installed, for + * frameworks that changed package name across major versions (React Router + * v6 ships as `react-router-dom`, v7 consolidated onto `react-router`). + * Checked alongside `packageName`. + */ + alternatePackageNames?: string[]; + /** Human-readable name for error messages (e.g., "Next.js") */ packageDisplayName: string; diff --git a/src/lib/programs/posthog-integration/index.ts b/src/lib/programs/posthog-integration/index.ts index 9dc2165c6..4519cb627 100644 --- a/src/lib/programs/posthog-integration/index.ts +++ b/src/lib/programs/posthog-integration/index.ts @@ -154,8 +154,12 @@ export const posthogIntegrationConfig: ProgramConfig = { installDir: session.installDir, }); if (packageJson) { - const { hasDeclaredDependency } = await import('@utils/package-json'); - if (!hasDeclaredDependency(config.detection.packageName, packageJson)) { + const { findDeclaredPackage } = await import('@utils/package-json'); + const accepted = [ + config.detection.packageName, + ...(config.detection.alternatePackageNames ?? []), + ]; + if (!findDeclaredPackage(accepted, packageJson)) { getUI().log.warn( `${config.detection.packageDisplayName} does not seem to be installed. Continuing anyway — the agent will handle it.`, );