Skip to content

Commit 158c604

Browse files
authored
feat(upgrade): handle non-standard semver versions (#7540)
1 parent ca4bb8f commit 158c604

File tree

8 files changed

+141
-9
lines changed

8 files changed

+141
-9
lines changed

.changeset/smooth-foxes-jump.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/upgrade': patch
3+
---
4+
5+
Handle `catalog:` protocol and other non-standard version specifiers
6+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "test-nextjs-catalog",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"@clerk/nextjs": "catalog:",
6+
"next": "^14.0.0",
7+
"react": "^18.0.0"
8+
}
9+
}

packages/upgrade/src/__tests__/fixtures/nextjs-catalog/pnpm-lock.yaml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { SignIn } from '@clerk/nextjs';
2+
3+
export default function Home() {
4+
return <SignIn />;
5+
}

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { getOldPackageName, getTargetPackageName, loadConfig } from '../../config.js';
3+
import { getAvailableReleases, getOldPackageName, getTargetPackageName, loadConfig } from '../../config.js';
44

55
describe('loadConfig', () => {
66
it('returns config with needsUpgrade: true for nextjs v6', async () => {
@@ -128,3 +128,46 @@ describe('getOldPackageName', () => {
128128
expect(getOldPackageName('nextjs')).toBeNull();
129129
});
130130
});
131+
132+
describe('getAvailableReleases', () => {
133+
it('returns an array of available releases', () => {
134+
const releases = getAvailableReleases();
135+
136+
expect(Array.isArray(releases)).toBe(true);
137+
expect(releases.length).toBeGreaterThan(0);
138+
});
139+
140+
it('includes core-3 release', () => {
141+
const releases = getAvailableReleases();
142+
143+
expect(releases).toContain('core-3');
144+
});
145+
146+
it('includes core-2 release', () => {
147+
const releases = getAvailableReleases();
148+
149+
expect(releases).toContain('core-2');
150+
});
151+
152+
it('returns releases in reverse order (newest first)', () => {
153+
const releases = getAvailableReleases();
154+
155+
expect(releases[0]).toBe('core-3');
156+
expect(releases[1]).toBe('core-2');
157+
});
158+
});
159+
160+
describe('loadConfig with null version', () => {
161+
it('returns config when release is explicitly provided', async () => {
162+
const config = await loadConfig('nextjs', null, 'core-3');
163+
164+
expect(config).not.toBeNull();
165+
expect(config.id).toBe('core-3');
166+
});
167+
168+
it('returns null when no release is provided and version is null', async () => {
169+
const config = await loadConfig('nextjs', null);
170+
171+
expect(config).toBeNull();
172+
});
173+
});

packages/upgrade/src/__tests__/integration/detect-sdk.test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ describe('getSdkVersion', () => {
5656
const version = getSdkVersion('nextjs', getFixturePath('no-clerk'));
5757
expect(version).toBeNull();
5858
});
59+
60+
it('returns null for catalog: protocol versions', () => {
61+
const version = getSdkVersion('nextjs', getFixturePath('nextjs-catalog'));
62+
expect(version).toBeNull();
63+
});
5964
});
6065

6166
describe('getMajorVersion', () => {
@@ -78,6 +83,18 @@ describe('getMajorVersion', () => {
7883
it('returns null for invalid semver', () => {
7984
expect(getMajorVersion('invalid')).toBeNull();
8085
});
86+
87+
it('returns null for catalog: protocol', () => {
88+
expect(getMajorVersion('catalog:')).toBeNull();
89+
});
90+
91+
it('returns null for catalog:default', () => {
92+
expect(getMajorVersion('catalog:default')).toBeNull();
93+
});
94+
95+
it('returns null for workspace: protocol', () => {
96+
expect(getMajorVersion('workspace:*')).toBeNull();
97+
});
8198
});
8299

83100
describe('normalizeSdkName', () => {

packages/upgrade/src/cli.js

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env node
22
import meow from 'meow';
33

4-
import { getOldPackageName, getTargetPackageName, loadConfig } from './config.js';
4+
import { getAvailableReleases, getOldPackageName, getTargetPackageName, loadConfig } from './config.js';
55
import {
66
createSpinner,
77
promptConfirm,
@@ -119,15 +119,52 @@ async function main() {
119119
const currentVersion = getSdkVersion(sdk, options.dir);
120120
const packageManager = detectPackageManager(options.dir);
121121

122-
// Step 3: Load version config
123-
const config = await loadConfig(sdk, currentVersion, options.release);
122+
// Step 3: If version couldn't be detected and no release specified, prompt user
123+
let release = options.release;
124+
125+
if (currentVersion === null && !release) {
126+
const availableReleases = getAvailableReleases();
127+
128+
if (availableReleases.length === 0) {
129+
renderError('No upgrade configurations found.');
130+
process.exit(1);
131+
}
132+
133+
renderWarning(
134+
`Could not detect your @clerk/${sdk} version (you may be using catalog: protocol or a non-standard version specifier).`,
135+
);
136+
renderNewline();
137+
138+
if (!isInteractive) {
139+
renderError('Please provide --release flag in non-interactive mode.');
140+
renderText('Available releases: ' + availableReleases.join(', '));
141+
process.exit(1);
142+
}
143+
144+
const releaseOptions = availableReleases.map(r => ({
145+
label: r.replace('-', ' ').replace(/\b\w/g, c => c.toUpperCase()),
146+
value: r,
147+
}));
148+
149+
release = await promptSelect('Which upgrade would you like to perform?', releaseOptions);
150+
151+
if (!release) {
152+
renderError('No release selected. Exiting.');
153+
process.exit(1);
154+
}
155+
156+
renderNewline();
157+
}
158+
159+
// Step 4: Load version config
160+
const config = await loadConfig(sdk, currentVersion, release);
124161

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

130-
// Step 4: Display configuration
167+
// Step 5: Display configuration
131168
renderConfig({
132169
sdk,
133170
currentVersion,
@@ -145,7 +182,7 @@ async function main() {
145182

146183
console.log('');
147184

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

158-
// Step 6: Run codemods
195+
// Step 7: Run codemods
159196
if (config.codemods?.length > 0) {
160197
renderText(`Running ${config.codemods.length} codemod(s)...`, 'blue');
161198
await runCodemods(config, sdk, options);
162199
renderSuccess('All codemods applied');
163200
renderNewline();
164201
}
165202

166-
// Step 7: Run scans
203+
// Step 8: Run scans
167204
if (config.changes?.length > 0) {
168205
renderText('Scanning for additional breaking changes...', 'blue');
169206
const results = await runScans(config, sdk, options);
170207
renderScanResults(results, config.docsUrl);
171208
}
172209

173-
// Step 8: Done
210+
// Step 9: Done
174211
renderComplete(sdk, config.docsUrl);
175212
}
176213

packages/upgrade/src/config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ import matter from 'gray-matter';
77
const __dirname = path.dirname(fileURLToPath(import.meta.url));
88
const VERSIONS_DIR = path.join(__dirname, 'versions');
99

10+
export function getAvailableReleases() {
11+
return fs
12+
.readdirSync(VERSIONS_DIR, { withFileTypes: true })
13+
.filter(d => d.isDirectory())
14+
.map(d => d.name)
15+
.filter(name => fs.existsSync(path.join(VERSIONS_DIR, name, 'index.js')))
16+
.sort()
17+
.reverse();
18+
}
19+
1020
export async function loadConfig(sdk, currentVersion, release) {
1121
const versionDirs = fs
1222
.readdirSync(VERSIONS_DIR, { withFileTypes: true })

0 commit comments

Comments
 (0)