diff --git a/src/components/application-starter/prompt-shared.ts b/src/components/application-starter/prompt-shared.ts index fdd641616..15979dfc3 100644 --- a/src/components/application-starter/prompt-shared.ts +++ b/src/components/application-starter/prompt-shared.ts @@ -4,6 +4,7 @@ import { composeApplicationStarterInput } from '~/utils/partners' import { type ApplicationStarterAnalysis, sanitizeStarterPrompt, + isNextJsMigrationInput, type ApplicationStarterRequest, type ApplicationStarterResult, } from '~/utils/application-starter' @@ -58,17 +59,8 @@ export type StarterPartnerButtonStyle = CSSProperties & { '--starter-partner-hover-border-color'?: string } -const nextJsMigrationPattern = - /migrat(?:e|ing|ion).*(next(?:\.js)?)|(next(?:\.js)?).*(migrat(?:e|ing|ion))/i -const remixMigrationPattern = - /migrat(?:e|ing|ion).*(remix|react[\s-]?router)|(remix|react[\s-]?router).*(migrat(?:e|ing|ion))/i const migrationRepositoryUrlPattern = /^(https?:\/\/\S+|git@\S+|ssh:\/\/\S+)$/i -export const STARTER_NEXTJS_MIGRATION_GUIDE_URL = - 'https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js' -export const STARTER_REMIX_MIGRATION_GUIDE_URL = - 'https://tanstack.com/router/latest/docs/guide/how-to/migrate-from-react-router' - export const starterPinnedLibraryIds = [ 'start', 'router', @@ -153,26 +145,6 @@ export function isPinnedStarterLibrary(libraryId: LibraryId) { ) } -export function isNextJsMigrationInput(input: string) { - return nextJsMigrationPattern.test(input) -} - -export function isRemixMigrationInput(input: string) { - return remixMigrationPattern.test(input) -} - -export function getStarterMigrationGuideUrl(input: string) { - if (isNextJsMigrationInput(input)) { - return STARTER_NEXTJS_MIGRATION_GUIDE_URL - } - - if (isRemixMigrationInput(input)) { - return STARTER_REMIX_MIGRATION_GUIDE_URL - } - - return null -} - export function normalizeMigrationRepositoryUrl(value: string) { return value.trim() } @@ -373,3 +345,11 @@ export class ApplicationStarterError extends Error { this.retryAfter = options?.retryAfter } } + +export { + isNextJsMigrationInput, + isRemixMigrationInput, + getStarterMigrationGuideUrl, + STARTER_NEXTJS_MIGRATION_GUIDE_URL, + STARTER_REMIX_MIGRATION_GUIDE_URL, +} from '~/utils/application-starter' diff --git a/src/utils/application-starter.ts b/src/utils/application-starter.ts index c048fed77..8791335cd 100644 --- a/src/utils/application-starter.ts +++ b/src/utils/application-starter.ts @@ -1278,6 +1278,7 @@ function buildPrompt({ if (resultType === 'migration') { const source = detectMigrationSource(userBrief) ?? 'the existing app' + const migrationGuideUrl = getStarterMigrationGuideUrl(userBrief) const migrationRepositoryInstructions = getMigrationRepositoryInstructionLines(input) @@ -1296,6 +1297,12 @@ function buildPrompt({ ...getStarterConstraintInstructionLines(input), ...migrationRepositoryInstructions, '', + ...(migrationGuideUrl + ? [ + `Fetch ${migrationGuideUrl} for the applicable routing and framework migration concepts. Keep the fresh TanStack project and the migration strategy below authoritative; do not apply in-place setup steps from the guide.`, + '', + ] + : []), 'Migration strategy:', '1. Inventory routes, layouts, auth, data loading, forms, and environment variables in the existing app.', '2. Recreate the destination structure in the fresh TanStack project first.', @@ -1475,3 +1482,33 @@ export function buildDownloadUrl(recipe: ApplicationStarterRecipe) { return `/api/application-starter/download?${params.toString()}` } + +const nextJsMigrationPattern = + /migrat(?:e|ing|ion)[\s\S]*\bnext(?:\.?js|\s+(?:app|application|project))\b|\bnext(?:\.?js|\s+(?:app|application|project))\b[\s\S]*migrat(?:e|ing|ion)/i +const remixMigrationPattern = + /migrat(?:e|ing|ion)[\s\S]*\b(remix|react[\s-]?router)\b|\b(remix|react[\s-]?router)\b[\s\S]*migrat(?:e|ing|ion)/i + +export const STARTER_NEXTJS_MIGRATION_GUIDE_URL = + 'https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js' +export const STARTER_REMIX_MIGRATION_GUIDE_URL = + 'https://tanstack.com/router/latest/docs/how-to/migrate-from-react-router' + +export function isNextJsMigrationInput(input: string) { + return nextJsMigrationPattern.test(input) +} + +export function isRemixMigrationInput(input: string) { + return remixMigrationPattern.test(input) +} + +export function getStarterMigrationGuideUrl(input: string) { + if (isNextJsMigrationInput(input)) { + return STARTER_NEXTJS_MIGRATION_GUIDE_URL + } + + if (isRemixMigrationInput(input)) { + return STARTER_REMIX_MIGRATION_GUIDE_URL + } + + return null +} diff --git a/tests/application-starter-partners.test.ts b/tests/application-starter-partners.test.ts index 8792d31ee..5bd61c509 100644 --- a/tests/application-starter-partners.test.ts +++ b/tests/application-starter-partners.test.ts @@ -14,6 +14,7 @@ require.extensions['.svg'] = loadAsset const { resolveApplicationStarterDeterministically, + getStarterMigrationGuideUrl, }: typeof import('../src/utils/application-starter') = require('../src/utils/application-starter') const { composeApplicationStarterInput, @@ -534,3 +535,54 @@ test('Cloudflare, Vercel, and Render rotate with the gold Start hosting partners assert.ok(partnerPositions.size > 1) } }) + +test('migration prompts include the matching migration guide', async () => { + for (const [source, guide] of [ + [ + 'Next.js', + 'https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js', + ], + [ + 'Remix', + 'https://tanstack.com/router/latest/docs/how-to/migrate-from-react-router', + ], + [ + 'React Router', + 'https://tanstack.com/router/latest/docs/how-to/migrate-from-react-router', + ], + ]) { + const result = await resolveApplicationStarterDeterministically({ + context: 'home', + input: `Migrate my ${source} app to TanStack Start.`, + }) + assert.ok(result.prompt.includes(`Fetch ${guide}`)) + assert.ok(result.prompt.includes('do not apply in-place setup steps')) + } + const result = await resolveApplicationStarterDeterministically({ + context: 'home', + input: 'Build a dashboard.', + }) + assert.equal(result.prompt.includes('Fetch https://tanstack.com/'), false) +}) + +test('migration guides distinguish framework names from timing and accept multiline briefs', () => { + assert.equal( + getStarterMigrationGuideUrl('Migrate my Remix app next week'), + 'https://tanstack.com/router/latest/docs/how-to/migrate-from-react-router', + ) + assert.equal(getStarterMigrationGuideUrl('Migrate my app next week'), null) + for (const input of [ + 'Migrate my app\nfrom Next.js to TanStack Start', + 'Next.js app\nneeds migration', + 'Migrate my Next app to TanStack Start', + ]) { + assert.equal( + getStarterMigrationGuideUrl(input), + 'https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js', + ) + } + assert.equal( + getStarterMigrationGuideUrl('Migrate my app\nfrom Remix to TanStack Start'), + 'https://tanstack.com/router/latest/docs/how-to/migrate-from-react-router', + ) +})