Skip to content

Commit e4a6dd3

Browse files
authored
fix(upgrade): add package replacement for @clerk/themes → @clerk/ui (#7932)
1 parent 6ec5f08 commit e4a6dd3

10 files changed

Lines changed: 139 additions & 1 deletion

File tree

.changeset/rude-pans-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/upgrade": patch
3+
---
4+
5+
fix(upgrade): add package replacement for @clerk/themes@clerk/ui
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "test-nextjs-v6-with-themes",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"@clerk/nextjs": "^6.0.0",
6+
"@clerk/themes": "^2.0.0",
7+
"next": "^14.0.0",
8+
"react": "^18.0.0"
9+
},
10+
"engines": {
11+
"node": ">=18.1.0"
12+
}
13+
}

packages/upgrade/src/__tests__/fixtures/nextjs-v6-with-themes/pnpm-lock.yaml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { dark } from '@clerk/themes';
2+
import { ClerkProvider } from '@clerk/nextjs';
3+
4+
export default function App() {
5+
return (
6+
<ClerkProvider appearance={{ baseTheme: dark }}>
7+
<div>Hello</div>
8+
</ClerkProvider>
9+
);
10+
}

packages/upgrade/src/__tests__/integration/cli.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,4 +317,47 @@ describe('CLI Integration', () => {
317317
expect(result.exitCode).toBe(1);
318318
});
319319
});
320+
321+
describe('Package Replacements', () => {
322+
let fixture;
323+
324+
beforeEach(() => {
325+
fixture = createTempFixture('nextjs-v6-with-themes');
326+
});
327+
328+
afterEach(() => {
329+
fixture?.cleanup();
330+
});
331+
332+
it('shows replacement message in dry-run mode', async () => {
333+
const result = await runCli(['--dir', fixture.path, '--dry-run', '--skip-codemods'], { timeout: 15000 });
334+
335+
expect(result.stdout).toContain('[dry run]');
336+
expect(result.stdout).toContain('@clerk/themes');
337+
expect(result.stdout).toContain('@clerk/ui');
338+
});
339+
340+
it('skips replacement when --skip-upgrade is used', async () => {
341+
const fs = await import('node:fs');
342+
const pkgBefore = fs.readFileSync(path.join(fixture.path, 'package.json'), 'utf8');
343+
344+
await runCli(['--dir', fixture.path, '--skip-upgrade', '--skip-codemods'], { timeout: 15000 });
345+
346+
const pkgAfter = fs.readFileSync(path.join(fixture.path, 'package.json'), 'utf8');
347+
expect(pkgAfter).toBe(pkgBefore);
348+
});
349+
350+
it('does not show replacement when package is not present', async () => {
351+
const noThemesFixture = createTempFixture('nextjs-v6');
352+
try {
353+
const result = await runCli(['--dir', noThemesFixture.path, '--dry-run', '--skip-codemods'], {
354+
timeout: 15000,
355+
});
356+
357+
expect(result.stdout).not.toContain('Would replace');
358+
} finally {
359+
noThemesFixture.cleanup();
360+
}
361+
});
362+
});
320363
});

packages/upgrade/src/cli.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import {
2727
} from './util/detect-sdk.js';
2828
import {
2929
detectPackageManager,
30+
getInstallCommand,
3031
getPackageManagerDisplayName,
32+
hasPackage,
3133
removePackage,
3234
upgradePackage,
3335
} from './util/package-manager.js';
@@ -245,6 +247,11 @@ async function main() {
245247
await performUpgrade(sdk, packageManager, config, options);
246248
}
247249

250+
// Step 6b: Handle package replacements
251+
if (config.packageReplacements?.length > 0 && !options.skipUpgrade) {
252+
await performPackageReplacements(packageManager, config, options);
253+
}
254+
248255
// Step 7: Run codemods
249256
if (config.codemods?.length > 0) {
250257
renderText(`Running ${config.codemods.length} codemod(s)...`, 'blue');
@@ -302,6 +309,49 @@ async function performUpgrade(sdk, packageManager, config, options) {
302309
}
303310
}
304311

312+
async function performPackageReplacements(packageManager, config, options) {
313+
const replacements = config.packageReplacements;
314+
if (!replacements?.length) {
315+
return;
316+
}
317+
318+
for (const { from, to } of replacements) {
319+
if (!hasPackage(from, options.dir)) {
320+
continue;
321+
}
322+
323+
const targetVersion = options.canary ? 'canary' : 'latest';
324+
325+
if (options.dryRun) {
326+
renderText(`[dry run] Would replace ${from} with ${to}@${targetVersion}`, 'yellow');
327+
continue;
328+
}
329+
330+
const removeSpinner = createSpinner(`Removing ${from}...`);
331+
try {
332+
await removePackage(packageManager, from, options.dir);
333+
removeSpinner.success(`Removed ${from}`);
334+
} catch (error) {
335+
removeSpinner.error(`Failed to remove ${from}`);
336+
renderError(error.message);
337+
renderWarning(`You may need to manually remove ${from} and install ${to}`);
338+
continue;
339+
}
340+
341+
const installSpinner = createSpinner(`Installing ${to}@${targetVersion}...`);
342+
try {
343+
await upgradePackage(packageManager, to, targetVersion, options.dir);
344+
installSpinner.success(`Installed ${to}@${targetVersion}`);
345+
} catch (error) {
346+
installSpinner.error(`Failed to install ${to}`);
347+
renderError(error.message);
348+
const [cmd, args] = getInstallCommand(packageManager, to, targetVersion, options.dir);
349+
renderWarning(`${from} was removed but ${to} could not be installed. Please run: ${cmd} ${args.join(' ')}`);
350+
throw new Error(`Package replacement failed: ${from} -> ${to}`);
351+
}
352+
}
353+
}
354+
305355
main().catch(error => {
306356
renderError(error.message);
307357
process.exit(1);

packages/upgrade/src/util/package-manager.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,16 @@ export function getPackageManagerDisplayName(packageManager) {
139139
return 'npm';
140140
}
141141
}
142+
143+
export function hasPackage(packageName, cwd) {
144+
const pkgPath = path.join(path.resolve(cwd), 'package.json');
145+
if (!fs.existsSync(pkgPath)) {
146+
return false;
147+
}
148+
try {
149+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
150+
return !!(pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName]);
151+
} catch {
152+
return false;
153+
}
154+
}

packages/upgrade/src/versions/core-3/changes/ui-themes-export-path.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ The `createTheme` theme utility has been moved to a new export path. Update your
1414
```
1515

1616
Note: The `__experimental_` prefix has been removed from the method since they're now in the `/themes/experimental` subpath.
17+
18+
The `@clerk/themes` package has been replaced by `@clerk/ui`. The upgrade CLI will automatically remove `@clerk/themes` and install `@clerk/ui` in your dependencies.

packages/upgrade/src/versions/core-3/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ export default {
3333
{ name: 'transform-satellite-auto-sync', packages: ['nextjs', 'react', 'expo', 'astro', 'tanstack-react-start'] },
3434
'transform-internal-clerk-js-ui-props',
3535
],
36+
packageReplacements: [{ from: '@clerk/themes', to: '@clerk/ui' }],
3637
};

playground/nextjs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
},
1111
"dependencies": {
1212
"@clerk/nextjs": "canary",
13-
"@clerk/themes": "canary",
13+
"@clerk/ui": "canary",
1414
"@clerk/types": "canary",
1515
"next": "^15",
1616
"react": "^19.1.1",

0 commit comments

Comments
 (0)