Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/smooth-foxes-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/upgrade': patch
---

Handle `catalog:` protocol and other non-standard version specifiers

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "test-nextjs-catalog",
"version": "1.0.0",
"dependencies": {
"@clerk/nextjs": "catalog:",
"next": "^14.0.0",
"react": "^18.0.0"
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SignIn } from '@clerk/nextjs';

export default function Home() {
return <SignIn />;
}
45 changes: 44 additions & 1 deletion packages/upgrade/src/__tests__/integration/config.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';

import { getOldPackageName, getTargetPackageName, loadConfig } from '../../config.js';
import { getAvailableReleases, getOldPackageName, getTargetPackageName, loadConfig } from '../../config.js';

describe('loadConfig', () => {
it('returns config with needsUpgrade: true for nextjs v6', async () => {
Expand Down Expand Up @@ -128,3 +128,46 @@ describe('getOldPackageName', () => {
expect(getOldPackageName('nextjs')).toBeNull();
});
});

describe('getAvailableReleases', () => {
it('returns an array of available releases', () => {
const releases = getAvailableReleases();

expect(Array.isArray(releases)).toBe(true);
expect(releases.length).toBeGreaterThan(0);
});

it('includes core-3 release', () => {
const releases = getAvailableReleases();

expect(releases).toContain('core-3');
});

it('includes core-2 release', () => {
const releases = getAvailableReleases();

expect(releases).toContain('core-2');
});

it('returns releases in reverse order (newest first)', () => {
const releases = getAvailableReleases();

expect(releases[0]).toBe('core-3');
expect(releases[1]).toBe('core-2');
});
});

describe('loadConfig with null version', () => {
it('returns config when release is explicitly provided', async () => {
const config = await loadConfig('nextjs', null, 'core-3');

expect(config).not.toBeNull();
expect(config.id).toBe('core-3');
});

it('returns null when no release is provided and version is null', async () => {
const config = await loadConfig('nextjs', null);

expect(config).toBeNull();
});
});
17 changes: 17 additions & 0 deletions packages/upgrade/src/__tests__/integration/detect-sdk.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ describe('getSdkVersion', () => {
const version = getSdkVersion('nextjs', getFixturePath('no-clerk'));
expect(version).toBeNull();
});

it('returns null for catalog: protocol versions', () => {
const version = getSdkVersion('nextjs', getFixturePath('nextjs-catalog'));
expect(version).toBeNull();
});
});

describe('getMajorVersion', () => {
Expand All @@ -78,6 +83,18 @@ describe('getMajorVersion', () => {
it('returns null for invalid semver', () => {
expect(getMajorVersion('invalid')).toBeNull();
});

it('returns null for catalog: protocol', () => {
expect(getMajorVersion('catalog:')).toBeNull();
});

it('returns null for catalog:default', () => {
expect(getMajorVersion('catalog:default')).toBeNull();
});

it('returns null for workspace: protocol', () => {
expect(getMajorVersion('workspace:*')).toBeNull();
});
});

describe('normalizeSdkName', () => {
Expand Down
53 changes: 45 additions & 8 deletions packages/upgrade/src/cli.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import meow from 'meow';

import { getOldPackageName, getTargetPackageName, loadConfig } from './config.js';
import { getAvailableReleases, getOldPackageName, getTargetPackageName, loadConfig } from './config.js';
import {
createSpinner,
promptConfirm,
Expand Down Expand Up @@ -119,15 +119,52 @@ async function main() {
const currentVersion = getSdkVersion(sdk, options.dir);
const packageManager = detectPackageManager(options.dir);

// Step 3: Load version config
const config = await loadConfig(sdk, currentVersion, options.release);
// Step 3: If version couldn't be detected and no release specified, prompt user
let release = options.release;

if (currentVersion === null && !release) {
const availableReleases = getAvailableReleases();

if (availableReleases.length === 0) {
renderError('No upgrade configurations found.');
process.exit(1);
}

renderWarning(
`Could not detect your @clerk/${sdk} version (you may be using catalog: protocol or a non-standard version specifier).`,
);
renderNewline();

if (!isInteractive) {
renderError('Please provide --release flag in non-interactive mode.');
renderText('Available releases: ' + availableReleases.join(', '));
process.exit(1);
}

const releaseOptions = availableReleases.map(r => ({
label: r.replace('-', ' ').replace(/\b\w/g, c => c.toUpperCase()),
value: r,
}));

release = await promptSelect('Which upgrade would you like to perform?', releaseOptions);

if (!release) {
renderError('No release selected. Exiting.');
process.exit(1);
}

renderNewline();
}

// Step 4: Load version config
const config = await loadConfig(sdk, currentVersion, release);

if (!config) {
renderError(`No upgrade path found for @clerk/${sdk}. Your version may be too old for this upgrade tool.`);
process.exit(1);
}

// Step 4: Display configuration
// Step 5: Display configuration
renderConfig({
sdk,
currentVersion,
Expand All @@ -145,7 +182,7 @@ async function main() {

console.log('');

// Step 5: Handle upgrade status
// Step 6: Handle upgrade status
if (options.skipUpgrade) {
renderText('Skipping package upgrade (--skip-upgrade flag)', 'yellow');
renderNewline();
Expand All @@ -155,22 +192,22 @@ async function main() {
await performUpgrade(sdk, packageManager, config, options);
}

// Step 6: Run codemods
// Step 7: Run codemods
if (config.codemods?.length > 0) {
renderText(`Running ${config.codemods.length} codemod(s)...`, 'blue');
await runCodemods(config, sdk, options);
renderSuccess('All codemods applied');
renderNewline();
}

// Step 7: Run scans
// Step 8: Run scans
if (config.changes?.length > 0) {
renderText('Scanning for additional breaking changes...', 'blue');
const results = await runScans(config, sdk, options);
renderScanResults(results, config.docsUrl);
}

// Step 8: Done
// Step 9: Done
renderComplete(sdk, config.docsUrl);
}

Expand Down
10 changes: 10 additions & 0 deletions packages/upgrade/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ import matter from 'gray-matter';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const VERSIONS_DIR = path.join(__dirname, 'versions');

export function getAvailableReleases() {
return fs
.readdirSync(VERSIONS_DIR, { withFileTypes: true })
.filter(d => d.isDirectory())
.map(d => d.name)
.filter(name => fs.existsSync(path.join(VERSIONS_DIR, name, 'index.js')))
.sort()
.reverse();
}

export async function loadConfig(sdk, currentVersion, release) {
const versionDirs = fs
.readdirSync(VERSIONS_DIR, { withFileTypes: true })
Expand Down
Loading