diff --git a/.changeset/upgrade-codemod-sdk-filtering.md b/.changeset/upgrade-codemod-sdk-filtering.md new file mode 100644 index 00000000000..88e6fdad9d6 --- /dev/null +++ b/.changeset/upgrade-codemod-sdk-filtering.md @@ -0,0 +1,5 @@ +--- +'@clerk/upgrade': minor +--- + +Add `transform-clerk-provider-inside-body` codemod for Next.js 16 cache components support diff --git a/packages/upgrade/src/__tests__/integration/runner.test.js b/packages/upgrade/src/__tests__/integration/runner.test.js index e3e5b2fcc72..957d6b0f477 100644 --- a/packages/upgrade/src/__tests__/integration/runner.test.js +++ b/packages/upgrade/src/__tests__/integration/runner.test.js @@ -1,9 +1,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { loadConfig } from '../../config.js'; -import { runScans } from '../../runner.js'; +import { runCodemods, runScans } from '../../runner.js'; import { createTempFixture } from '../helpers/create-fixture.js'; +const mockRunCodemod = vi.fn(() => Promise.resolve({ stats: {} })); + +vi.mock('../../codemods/index.js', () => ({ + runCodemod: (...args) => mockRunCodemod(...args), + getCodemodConfig: vi.fn(() => null), +})); + vi.mock('../../render.js', () => ({ colors: { reset: '', bold: '', yellow: '', gray: '' }, createSpinner: vi.fn(() => ({ @@ -17,6 +24,122 @@ vi.mock('../../render.js', () => ({ renderText: vi.fn(), })); +describe('runCodemods', () => { + beforeEach(() => { + mockRunCodemod.mockClear(); + }); + + it('runs all codemods when no packages filter is specified', async () => { + const config = { + codemods: ['transform-a', 'transform-b'], + }; + + await runCodemods(config, 'nextjs', { glob: '**/*.tsx' }); + + expect(mockRunCodemod).toHaveBeenCalledTimes(2); + expect(mockRunCodemod).toHaveBeenCalledWith('transform-a', ['**/*.tsx'], { glob: '**/*.tsx' }); + expect(mockRunCodemod).toHaveBeenCalledWith('transform-b', ['**/*.tsx'], { glob: '**/*.tsx' }); + }); + + it('skips codemods that do not match the current SDK', async () => { + const config = { + codemods: [ + 'transform-all', // runs for all + { name: 'transform-nextjs-only', packages: ['nextjs'] }, + { name: 'transform-react-only', packages: ['react'] }, + ], + }; + + await runCodemods(config, 'nextjs', { glob: '**/*.tsx' }); + + expect(mockRunCodemod).toHaveBeenCalledTimes(2); + expect(mockRunCodemod).toHaveBeenCalledWith('transform-all', ['**/*.tsx'], { glob: '**/*.tsx' }); + expect(mockRunCodemod).toHaveBeenCalledWith('transform-nextjs-only', ['**/*.tsx'], { glob: '**/*.tsx' }); + }); + + it('runs codemods with wildcard packages for any SDK', async () => { + const config = { + codemods: [{ name: 'transform-universal', packages: ['*'] }], + }; + + await runCodemods(config, 'expo', { glob: '**/*.tsx' }); + + expect(mockRunCodemod).toHaveBeenCalledTimes(1); + expect(mockRunCodemod).toHaveBeenCalledWith('transform-universal', ['**/*.tsx'], { glob: '**/*.tsx' }); + }); + + it('runs codemods when SDK is in the packages array', async () => { + const config = { + codemods: [{ name: 'transform-multi', packages: ['nextjs', 'react', 'expo'] }], + }; + + await runCodemods(config, 'react', { glob: '**/*.tsx' }); + + expect(mockRunCodemod).toHaveBeenCalledTimes(1); + }); + + it('skips all codemods when SDK does not match any', async () => { + const config = { + codemods: [ + { name: 'transform-nextjs-only', packages: ['nextjs'] }, + { name: 'transform-react-only', packages: ['react'] }, + ], + }; + + await runCodemods(config, 'expo', { glob: '**/*.tsx' }); + + expect(mockRunCodemod).not.toHaveBeenCalled(); + }); + + it('handles empty codemods array', async () => { + const config = { + codemods: [], + }; + + await runCodemods(config, 'nextjs', { glob: '**/*.tsx' }); + + expect(mockRunCodemod).not.toHaveBeenCalled(); + }); + + it('handles undefined codemods', async () => { + const config = {}; + + await runCodemods(config, 'nextjs', { glob: '**/*.tsx' }); + + expect(mockRunCodemod).not.toHaveBeenCalled(); + }); + + it('treats empty packages array as matching no SDKs', async () => { + const config = { + codemods: [{ name: 'transform-none', packages: [] }], + }; + + await runCodemods(config, 'nextjs', { glob: '**/*.tsx' }); + + expect(mockRunCodemod).not.toHaveBeenCalled(); + }); + + it('treats undefined packages as matching all SDKs', async () => { + const config = { + codemods: [{ name: 'transform-all', packages: undefined }], + }; + + await runCodemods(config, 'expo', { glob: '**/*.tsx' }); + + expect(mockRunCodemod).toHaveBeenCalledTimes(1); + }); + + it('propagates errors from codemod execution', async () => { + mockRunCodemod.mockRejectedValueOnce(new Error('Codemod failed')); + + const config = { + codemods: ['transform-broken'], + }; + + await expect(runCodemods(config, 'nextjs', { glob: '**/*.tsx' })).rejects.toThrow('Codemod failed'); + }); +}); + describe('runScans', () => { let fixture; diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-clerk-provider-inside-body.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-clerk-provider-inside-body.fixtures.js new file mode 100644 index 00000000000..43806155cff --- /dev/null +++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-clerk-provider-inside-body.fixtures.js @@ -0,0 +1,267 @@ +export const fixtures = [ + { + name: 'Moves ClerkProvider from wrapping html to inside body', + source: ` +import { ClerkProvider } from '@clerk/nextjs' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ); +} + `, + output: ` +import { ClerkProvider } from '@clerk/nextjs' + +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} + `, + }, + { + name: 'Preserves ClerkProvider props when moving', + source: ` +import { ClerkProvider } from '@clerk/nextjs' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ); +} + `, + output: ` +import { ClerkProvider } from '@clerk/nextjs' + +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} + `, + }, + { + name: 'Does not transform if ClerkProvider is already inside body', + source: ` +import { ClerkProvider } from '@clerk/nextjs' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ); +} + `, + output: '', + }, + { + name: 'Does not transform if ClerkProvider does not wrap html', + source: ` +import { ClerkProvider } from '@clerk/nextjs' + +export default function Page() { + return ( + +
{children}
+
+ ); +} + `, + output: '', + }, + { + name: 'Does not transform if not from @clerk/nextjs', + source: ` +import { ClerkProvider } from 'some-other-package' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ); +} + `, + output: '', + }, + { + name: 'Handles body with multiple children', + source: ` +import { ClerkProvider } from '@clerk/nextjs' + +export default function RootLayout({ children }) { + return ( + + + +
+
{children}
+