@clerk/upgrade 2.0.0-snapshot.v20251208202852 → 2.0.0-snapshot.v20251211120550

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,25 +27,55 @@
27
27
 
28
28
  ## Getting Started
29
29
 
30
- A tool that helps with upgrading major versions of Clerk's SDKs.
30
+ A CLI that detects your installed Clerk SDK, upgrades packages to the next major release, runs codemods, and scans your codebase for breaking changes.
31
31
 
32
32
  ### Prerequisites
33
33
 
34
- - Node.js `>=18.17.0` or later
34
+ - Node.js `>=20.9.0`
35
+ - pnpm, npm, or yarn installed in the target project
35
36
 
36
37
  ## Usage
37
38
 
38
- Navigate to the application you want to upgrade and run the following in your terminal:
39
+ Run the CLI from the root of the project you want to upgrade:
39
40
 
40
41
  ```sh
42
+ # with pnpm
43
+ pnpm dlx @clerk/upgrade
44
+
45
+ # with npm
41
46
  npx @clerk/upgrade
47
+
48
+ # if installed locally
49
+ pnpm clerk-upgrade
42
50
  ```
43
51
 
44
- Fill out the questionnaire and the CLI will show you the required changes.
52
+ Fill out the prompts and the CLI will:
53
+
54
+ - Detect your Clerk SDK and version (or prompt for `--sdk`)
55
+ - Upgrade packages (including renames such as `@clerk/clerk-react` → `@clerk/react`)
56
+ - Run codemods for the selected release
57
+ - Scan your files for breaking changes and print a report
58
+
59
+ ### CLI options
60
+
61
+ - `--sdk` — SDK to upgrade (e.g., `nextjs`, `react`, `expo`); required in non-interactive runs if detection fails
62
+ - `--dir` — directory to scan (default: current working directory)
63
+ - `--glob` — glob of files for codemods (default: `**/*.(js|jsx|ts|tsx|mjs|cjs)`)
64
+ - `--ignore` — extra globs to ignore during scans (repeatable)
65
+ - `--release` — target release (e.g., `core-3`); otherwise auto-selected from installed versions
66
+ - `--skip-upgrade` — skip installing/updating packages
67
+ - `--skip-codemods` — skip codemod execution
68
+ - `--dry-run` — show what would change without writing or installing
69
+
70
+ ### Releases
71
+
72
+ - `core-3`: upgrades Next.js 6 → 7, React 5 → 7, Expo 2 → 3, React Router 2 → 3, TanStack React Start 0 → 1, Astro 2 → 3, Nuxt 2 → 3, Vue 2 → 3; includes package renames for React/Expo
73
+
74
+ The CLI selects the appropriate release automatically based on the installed major version, or you can pin a release with `--release` (currently `core-3`).
45
75
 
46
76
  ### Caveats
47
77
 
48
- This tool uses regular expressions to scan for patterns that match breaking changes. This makes it run substantially faster and makes it more accessible for us at Clerk to author matchers for each breaking change, however it means that _we cannot gurarantee 100% accuracy of the results_. As such, it's important to treat this as a tool that can help you to complete your major version upgrades, rather than an automatic fix to all issues.
78
+ Scans rely on regular expressions, so some patterns (unusual imports, bound methods, indirect calls) can be missed. Codemods cover common upgrades, but you should still review the report, run your test suite, and verify the app before deploying.
49
79
 
50
80
  The main thing that this tool will miss is cases where _unusual import patterns_ are used in your codebase. As an example, if Clerk made a breaking change to the `getAuth` function exported from `@clerk/nextjs`, `@clerk/upgrade` would likely look for something like `import { getAuth } from "@clerk/nextjs"` in order to detect whether you need to make some changes. If you were using your imports like `import * as ClerkNext from "@clerk/nextjs"`, you could use `getAuth` without it detecting it with its matcher.
51
81
 
@@ -1,8 +1,8 @@
1
- import { ClerkProvider, useAuth, useUser } from '@clerk/nextjs';
2
- import { dark } from '@clerk/nextjs/themes';
1
+ import { ClerkProvider, useAuth } from '@clerk/nextjs';
2
+ import { useUser } from '@clerk/clerk-react';
3
3
 
4
4
  export default function App({ children }) {
5
- return <ClerkProvider appearance={{ baseTheme: dark }}>{children}</ClerkProvider>;
5
+ return <ClerkProvider>{children}</ClerkProvider>;
6
6
  }
7
7
 
8
8
  export function UserProfile() {
@@ -74,6 +74,8 @@ describe('CLI Integration', () => {
74
74
  expect(result.stdout).toContain('--sdk');
75
75
  expect(result.stdout).toContain('--dir');
76
76
  expect(result.stdout).toContain('--dry-run');
77
+ expect(result.stdout).toContain('--skip-upgrade');
78
+ expect(result.stdout).toContain('--release');
77
79
  expect(result.exitCode).toBe(0);
78
80
  });
79
81
  });
@@ -227,4 +229,47 @@ describe('CLI Integration', () => {
227
229
  expect(result.stdout).toContain('codemod');
228
230
  });
229
231
  });
232
+ describe('--skip-upgrade flag', () => {
233
+ let fixture;
234
+ beforeEach(() => {
235
+ fixture = createTempFixture('nextjs-v6');
236
+ });
237
+ afterEach(() => {
238
+ fixture?.cleanup();
239
+ });
240
+ it('skips the package upgrade step', async () => {
241
+ const result = await runCli(['--dir', fixture.path, '--skip-upgrade', '--skip-codemods'], {
242
+ timeout: 15000
243
+ });
244
+ expect(result.stdout).toContain('Skipping package upgrade');
245
+ expect(result.stdout).toContain('--skip-upgrade');
246
+ });
247
+ it('does not modify package.json when skipping upgrade', async () => {
248
+ const fs = await import('node:fs');
249
+ const pkgBefore = fs.readFileSync(path.join(fixture.path, 'package.json'), 'utf8');
250
+ await runCli(['--dir', fixture.path, '--skip-upgrade', '--skip-codemods'], {
251
+ timeout: 15000
252
+ });
253
+ const pkgAfter = fs.readFileSync(path.join(fixture.path, 'package.json'), 'utf8');
254
+ expect(pkgAfter).toBe(pkgBefore);
255
+ });
256
+ });
257
+ describe('--release flag', () => {
258
+ it('loads specific release configuration', async () => {
259
+ const dir = getFixturePath('nextjs-v7');
260
+ const result = await runCli(['--dir', dir, '--release', 'core-3', '--dry-run', '--skip-codemods'], {
261
+ timeout: 15000
262
+ });
263
+ expect(result.stdout).toContain('core-3');
264
+ });
265
+ it('errors when release does not exist', async () => {
266
+ const dir = getFixturePath('nextjs-v6');
267
+ const result = await runCli(['--dir', dir, '--release', 'nonexistent-release', '--dry-run', '--skip-codemods'], {
268
+ timeout: 15000
269
+ });
270
+ const output = result.stdout + result.stderr;
271
+ expect(output).toContain('No upgrade path found');
272
+ expect(result.exitCode).toBe(1);
273
+ });
274
+ });
230
275
  });
@@ -45,6 +45,27 @@ describe('loadConfig', () => {
45
45
  expect(config.docsUrl).toBeDefined();
46
46
  expect(config.docsUrl).toContain('clerk.com');
47
47
  });
48
+ describe('release parameter', () => {
49
+ it('loads specific release when provided', async () => {
50
+ const config = await loadConfig('nextjs', 7, 'core-3');
51
+ expect(config).not.toBeNull();
52
+ expect(config.id).toBe('core-3');
53
+ });
54
+ it('returns null for non-existent release', async () => {
55
+ const config = await loadConfig('nextjs', 6, 'nonexistent-release');
56
+ expect(config).toBeNull();
57
+ });
58
+ it('ignores version status when release is specified', async () => {
59
+ const config = await loadConfig('nextjs', 7, 'core-3');
60
+ expect(config).not.toBeNull();
61
+ expect(config.alreadyUpgraded).toBe(true);
62
+ });
63
+ it('loads changes for specific release', async () => {
64
+ const config = await loadConfig('nextjs', 6, 'core-3');
65
+ expect(config.changes).toBeDefined();
66
+ expect(Array.isArray(config.changes)).toBe(true);
67
+ });
68
+ });
48
69
  });
49
70
  describe('getTargetPackageName', () => {
50
71
  it('returns @clerk/react for react sdk', () => {
@@ -34,7 +34,7 @@ describe('runScans', () => {
34
34
  ignore: []
35
35
  };
36
36
  const results = await runScans(config, 'nextjs', options);
37
- expect(Array.isArray(results)).toBe(true);
37
+ expect(results.length).toBeGreaterThan(0);
38
38
  });
39
39
  it('returns empty array when no matchers match', async () => {
40
40
  const config = await loadConfig('nextjs', 6);
@@ -53,27 +53,6 @@ describe('runScans', () => {
53
53
  ignore: ['**/src/**']
54
54
  };
55
55
  const results = await runScans(config, 'nextjs', options);
56
- expect(Array.isArray(results)).toBe(true);
57
- });
58
- });
59
- describe('runScans with theme imports', () => {
60
- let fixture;
61
- beforeEach(() => {
62
- fixture = createTempFixture('nextjs-v6');
63
- });
64
- afterEach(() => {
65
- fixture?.cleanup();
66
- });
67
- it('detects theme imports from @clerk/nextjs/themes', async () => {
68
- const config = await loadConfig('nextjs', 6);
69
- const options = {
70
- dir: fixture.path,
71
- ignore: []
72
- };
73
- const results = await runScans(config, 'nextjs', options);
74
- const themeChange = results.find(r => r.title?.includes('Theme') || r.docsAnchor?.includes('theme'));
75
- if (themeChange) {
76
- expect(themeChange.instances.length).toBeGreaterThan(0);
77
- }
56
+ expect(results).toEqual([]);
78
57
  });
79
58
  });
package/dist/cli.js CHANGED
@@ -15,6 +15,8 @@ const cli = meow(`
15
15
  --dir Directory to scan (defaults to current directory)
16
16
  --glob Glob pattern for files to transform (defaults to **/*.{js,jsx,ts,tsx,mjs,cjs})
17
17
  --ignore Directories/files to ignore (can be used multiple times)
18
+ --skip-upgrade Skip the upgrade step
19
+ --release Name of the release you're upgrading to (e.g. core-3)
18
20
  --dry-run Show what would be done without making changes
19
21
 
20
22
  Examples
@@ -43,6 +45,13 @@ const cli = meow(`
43
45
  type: 'string',
44
46
  isMultiple: true
45
47
  },
48
+ skipUpgrade: {
49
+ type: 'boolean',
50
+ default: false
51
+ },
52
+ release: {
53
+ type: 'string'
54
+ },
46
55
  dryRun: {
47
56
  type: 'boolean',
48
57
  default: false
@@ -59,6 +68,8 @@ async function main() {
59
68
  dir: cli.flags.dir,
60
69
  glob: cli.flags.glob,
61
70
  ignore: cli.flags.ignore,
71
+ skipUpgrade: cli.flags.skipUpgrade,
72
+ release: cli.flags.release,
62
73
  dryRun: cli.flags.dryRun,
63
74
  skipCodemods: cli.flags.skipCodemods
64
75
  };
@@ -94,7 +105,7 @@ async function main() {
94
105
  const packageManager = detectPackageManager(options.dir);
95
106
 
96
107
  // Step 3: Load version config
97
- const config = await loadConfig(sdk, currentVersion);
108
+ const config = await loadConfig(sdk, currentVersion, options.release);
98
109
  if (!config) {
99
110
  renderError(`No upgrade path found for @clerk/${sdk}. Your version may be too old for this upgrade tool.`);
100
111
  process.exit(1);
@@ -110,14 +121,17 @@ async function main() {
110
121
  dir: options.dir,
111
122
  packageManager: getPackageManagerDisplayName(packageManager)
112
123
  });
113
- if (isInteractive && !(await promptConfirm('Ready to upgrade?'))) {
124
+ if (isInteractive && !(await promptConfirm('Ready to upgrade?', true))) {
114
125
  renderError('Upgrade cancelled. Exiting...');
115
126
  process.exit(0);
116
127
  }
117
128
  console.log('');
118
129
 
119
130
  // Step 5: Handle upgrade status
120
- if (config.alreadyUpgraded) {
131
+ if (options.skipUpgrade) {
132
+ renderText('Skipping package upgrade (--skip-upgrade flag)', 'yellow');
133
+ renderNewline();
134
+ } else if (config.alreadyUpgraded) {
121
135
  renderSuccess(`You're already on the latest major version of @clerk/${sdk}`);
122
136
  } else if (config.needsUpgrade) {
123
137
  await performUpgrade(sdk, packageManager, config, options);
@@ -65,4 +65,28 @@ createClerkClient();
65
65
  output: `
66
66
  <OrganizationProfile />;
67
67
  `
68
+ }, {
69
+ name: 'Does not rename class constructors',
70
+ source: `
71
+ export class AppError extends Error {
72
+ constructor(
73
+ message: string,
74
+ public readonly code: string,
75
+ public readonly statusCode: number = 500
76
+ ) {
77
+ super(message);
78
+ }
79
+ }
80
+ `,
81
+ output: `
82
+ export class AppError extends Error {
83
+ constructor(
84
+ message: string,
85
+ public readonly code: string,
86
+ public readonly statusCode: number = 500
87
+ ) {
88
+ super(message);
89
+ }
90
+ }
91
+ `
68
92
  }];
@@ -9,7 +9,7 @@ describe('transform-align-experimental-unstable-prefixes', () => {
9
9
  }) => {
10
10
  const result = applyTransform(transformer, {}, {
11
11
  source
12
- });
12
+ }) || source.trim();
13
13
  expect(result).toEqual(output.trim());
14
14
  });
15
15
  });
@@ -9,7 +9,7 @@ const CODEMOD_CONFIG = {
9
9
  renderSummary: renderDeprecatedPropsSummary
10
10
  }
11
11
  };
12
- const GLOBBY_IGNORE = ['**/*.md', 'node_modules/**', '**/node_modules/**', '.git/**', '**/*.json', 'package.json', '**/package.json', 'package-lock.json', '**/package-lock.json', 'yarn.lock', '**/yarn.lock', 'pnpm-lock.yaml', '**/pnpm-lock.yaml', 'yalc.lock', '**/*.(ico|png|webp|svg|gif|jpg|jpeg)+', '**/*.(mp4|mkv|wmv|m4v|mov|avi|flv|webm|flac|mka|m4a|aac|ogg)+', '**/*.(css|scss|sass|less|styl)+'];
12
+ const GLOBBY_IGNORE = ['**/*.md', 'node_modules/**', '**/node_modules/**', '.git/**', '**/*.json', 'package.json', '**/package.json', 'package-lock.json', '**/package-lock.json', 'yarn.lock', '**/yarn.lock', 'pnpm-lock.yaml', '**/pnpm-lock.yaml', 'yalc.lock', '**/*.{ico,png,webp,svg,gif,jpg,jpeg}', '**/*.{mp4,mkv,wmv,m4v,mov,avi,flv,webm,flac,mka,m4a,aac,ogg}', '**/*.{css,scss,sass,less,styl}'];
13
13
  export async function runCodemod(transform = 'transform-async-request', glob, options = {}) {
14
14
  if (!transform) {
15
15
  throw new Error('No transform provided');
@@ -1,18 +1,18 @@
1
- const SPECIFIC_RENAMES = {
2
- experimental_createTheme: 'createTheme',
1
+ const SPECIFIC_RENAMES = Object.freeze({
3
2
  __experimental_createTheme: 'createTheme',
4
- experimental__simple: 'simple',
5
3
  __experimental_simple: 'simple',
6
4
  __unstable__createClerkClient: 'createClerkClient',
7
- __unstable_invokeMiddlewareOnAuthStateChange: '__internal_invokeMiddlewareOnAuthStateChange',
8
5
  __unstable__environment: '__internal_environment',
9
- __unstable__updateProps: '__internal_updateProps',
10
- __unstable__setEnvironment: '__internal_setEnvironment',
11
- __unstable__onBeforeRequest: '__internal_onBeforeRequest',
12
6
  __unstable__onAfterResponse: '__internal_onAfterResponse',
7
+ __unstable__onAfterSetActive: '__internal_onAfterSetActive',
8
+ __unstable__onBeforeRequest: '__internal_onBeforeRequest',
13
9
  __unstable__onBeforeSetActive: '__internal_onBeforeSetActive',
14
- __unstable__onAfterSetActive: '__internal_onAfterSetActive'
15
- };
10
+ __unstable__setEnvironment: '__internal_setEnvironment',
11
+ __unstable__updateProps: '__internal_updateProps',
12
+ __unstable_invokeMiddlewareOnAuthStateChange: '__internal_invokeMiddlewareOnAuthStateChange',
13
+ experimental__simple: 'simple',
14
+ experimental_createTheme: 'createTheme'
15
+ });
16
16
  const REMOVED_PROPS = new Set(['__unstable_manageBillingUrl', '__unstable_manageBillingLabel', '__unstable_manageBillingMembersLimit', 'experimental__forceOauthFirst']);
17
17
  const UI_THEME_NAMES = new Set(['createTheme', 'simple', 'experimental_createTheme', '__experimental_createTheme', 'experimental__simple', '__experimental_simple']);
18
18
  const UI_THEME_SOURCE = '@clerk/ui/themes/experimental';
@@ -20,6 +20,18 @@ const UI_LEGACY_SOURCES = new Set(['@clerk/ui', '@clerk/ui/themes', UI_THEME_SOU
20
20
  const CHROME_CLIENT_NAMES = new Set(['__unstable__createClerkClient', 'createClerkClient']);
21
21
  const CHROME_BACKGROUND_SOURCE = '@clerk/chrome-extension/background';
22
22
  const CHROME_LEGACY_SOURCE = '@clerk/chrome-extension';
23
+
24
+ /**
25
+ * Transforms experimental and unstable prefixed identifiers to their stable or internal equivalents.
26
+ * Also moves theme-related imports to @clerk/ui/themes/experimental and Chrome extension imports
27
+ * to @clerk/chrome-extension/background. Removes deprecated billing-related props.
28
+ *
29
+ * @param {Object} file - The file object containing the source code
30
+ * @param {string} file.source - The source code to transform
31
+ * @param {Object} api - The jscodeshift API
32
+ * @param {Function} api.jscodeshift - The jscodeshift function
33
+ * @returns {string|undefined} The transformed source code, or undefined if no changes were made
34
+ */
23
35
  module.exports = function transformAlignExperimentalUnstablePrefixes({
24
36
  source
25
37
  }, {
@@ -28,10 +40,10 @@ module.exports = function transformAlignExperimentalUnstablePrefixes({
28
40
  const root = j(source);
29
41
  let dirty = false;
30
42
  const maybeRename = name => {
31
- if (!name || REMOVED_PROPS.has(name)) {
43
+ if (!name || REMOVED_PROPS.has(name) || !Object.hasOwn(SPECIFIC_RENAMES, name)) {
32
44
  return null;
33
45
  }
34
- return SPECIFIC_RENAMES[name] ?? null;
46
+ return SPECIFIC_RENAMES[name];
35
47
  };
36
48
  const renameIdentifier = node => {
37
49
  const newName = maybeRename(node.name);
@@ -164,7 +176,7 @@ module.exports = function transformAlignExperimentalUnstablePrefixes({
164
176
  }
165
177
  });
166
178
  });
167
- root.find(j.Identifier).forEach(path => {
179
+ root.find(j.Identifier).filter(path => maybeRename(path.node.name)).forEach(path => {
168
180
  renameIdentifier(path.node);
169
181
  });
170
182
  root.find(j.JSXOpeningElement).forEach(path => {
package/dist/config.js CHANGED
@@ -4,10 +4,33 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
4
4
  import matter from 'gray-matter';
5
5
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
6
  const VERSIONS_DIR = path.join(__dirname, 'versions');
7
- export async function loadConfig(sdk, currentVersion) {
7
+ export async function loadConfig(sdk, currentVersion, release) {
8
8
  const versionDirs = fs.readdirSync(VERSIONS_DIR, {
9
9
  withFileTypes: true
10
10
  }).filter(d => d.isDirectory()).map(d => d.name);
11
+
12
+ // If a specific release is requested, load it directly
13
+ if (release) {
14
+ if (!versionDirs.includes(release)) {
15
+ return null;
16
+ }
17
+ const configPath = path.join(VERSIONS_DIR, release, 'index.js');
18
+ if (!fs.existsSync(configPath)) {
19
+ return null;
20
+ }
21
+ const moduleUrl = pathToFileURL(configPath).href;
22
+ const mod = await import(moduleUrl);
23
+ const config = mod.default ?? mod;
24
+ const changes = loadChanges(release, sdk);
25
+ const versionStatus = getVersionStatus(config, sdk, currentVersion);
26
+ return {
27
+ ...config,
28
+ changes,
29
+ versionStatus,
30
+ needsUpgrade: versionStatus === 'needs-upgrade',
31
+ alreadyUpgraded: versionStatus === 'already-upgraded'
32
+ };
33
+ }
11
34
  let applicableConfig = null;
12
35
  for (const versionDir of versionDirs) {
13
36
  const configPath = path.join(VERSIONS_DIR, versionDir, 'index.js');
package/dist/render.js CHANGED
@@ -43,15 +43,21 @@ export function renderConfig({
43
43
  }
44
44
  console.log('');
45
45
  }
46
- export async function promptConfirm(message) {
46
+ export async function promptConfirm(message, defaultYes = false) {
47
47
  const rl = readline.createInterface({
48
48
  input: process.stdin,
49
49
  output: process.stdout
50
50
  });
51
51
  return new Promise(resolve => {
52
- rl.question(`${message} (y/n): `, answer => {
52
+ const prompt = defaultYes ? `${message} (Y/n): ` : `${message} (y/N): `;
53
+ rl.question(prompt, answer => {
53
54
  rl.close();
54
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
55
+ const normalized = answer.trim().toLowerCase();
56
+ if (!normalized) {
57
+ resolve(defaultYes);
58
+ return;
59
+ }
60
+ resolve(normalized === 'y' || normalized === 'yes');
55
61
  });
56
62
  });
57
63
  }
package/dist/runner.js CHANGED
@@ -5,7 +5,7 @@ import { convertPathToPattern, globby } from 'globby';
5
5
  import indexToPosition from 'index-to-position';
6
6
  import { getCodemodConfig, runCodemod } from './codemods/index.js';
7
7
  import { createSpinner, renderCodemodResults } from './render.js';
8
- const GLOBBY_IGNORE = ['node_modules/**', '**/node_modules/**', '.git/**', 'dist/**', '**/dist/**', 'build/**', '**/build/**', '.next/**', '**/.next/**', 'package.json', '**/package.json', 'package-lock.json', '**/package-lock.json', 'yarn.lock', '**/yarn.lock', 'pnpm-lock.yaml', '**/pnpm-lock.yaml', '**/*.(png|webp|svg|gif|jpg|jpeg)+', '**/*.(mp4|mkv|wmv|m4v|mov|avi|flv|webm|flac|mka|m4a|aac|ogg)+'];
8
+ const GLOBBY_IGNORE = ['node_modules/**', '**/node_modules/**', '.git/**', 'dist/**', '**/dist/**', 'build/**', '**/build/**', '.next/**', '**/.next/**', 'package.json', '**/package.json', 'package-lock.json', '**/package-lock.json', 'yarn.lock', '**/yarn.lock', 'pnpm-lock.yaml', '**/pnpm-lock.yaml', '**/*.{png,webp,svg,gif,jpg,jpeg}', '**/*.{mp4,mkv,wmv,m4v,mov,avi,flv,webm,flac,mka,m4a,aac,ogg}'];
9
9
  export async function runCodemods(config, sdk, options) {
10
10
  const codemods = config.codemods || [];
11
11
  if (codemods.length === 0) {
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: '`@clerk/clerk-react` renamed to `@clerk/react`'
3
- packages: ['react']
4
3
  matcher: '@clerk/clerk-react'
5
4
  category: 'breaking'
6
5
  docsAnchor: 'clerk-react-rename'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clerk/upgrade",
3
- "version": "2.0.0-snapshot.v20251208202852",
3
+ "version": "2.0.0-snapshot.v20251211120550",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/clerk/javascript.git",