@clerk/upgrade 1.0.9 → 1.1.0-canary.v1997eac

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/dist/app.js CHANGED
@@ -1,35 +1,46 @@
1
1
  import { MultiSelect, Select, TextInput } from '@inkjs/ui';
2
- import { Newline, Text } from 'ink';
3
- import BigText from 'ink-big-text';
4
- import Gradient from 'ink-gradient';
5
- import React, { useState } from 'react';
2
+ import { Newline, Text, useApp } from 'ink';
3
+ import React, { useEffect, useState } from 'react';
4
+ import { Header } from './components/Header.js';
5
+ import { Scan } from './components/Scan.js';
6
+ import { SDKWorkflow } from './components/SDKWorkflow.js';
6
7
  import SDKS from './constants/sdks.js';
7
- import Scan from './scan.js';
8
- import getClerkMajorVersion from './util/get-clerk-version.js';
8
+ import { getClerkMajorVersion } from './util/get-clerk-version.js';
9
9
  import guessFrameworks from './util/guess-framework.js';
10
- export default function App({
11
- _fromVersion,
12
- _toVersion,
13
- _sdk,
14
- _dir = false,
15
- _ignore = [],
16
- _yolo = false,
17
- noWarnings = false,
18
- disableTelemetry = false
19
- }) {
20
- const [yolo, setYolo] = useState(_yolo);
21
- const [sdks, setSdks] = useState(_sdk ? [_sdk] : []);
10
+
11
+ /**
12
+ * Main CLI application component for handling Clerk SDK upgrades.
13
+ *
14
+ * @param {Object} props - The `props` object.
15
+ * @param {string} [props.dir] - The directory to scan for files.
16
+ * @param {boolean} [props.disableTelemetry=false] - Flag to disable telemetry.
17
+ * @param {string} [props.fromVersion] - The current version of the SDK.
18
+ * @param {Array<string>} [props.ignore] - List of files or directories to ignore.
19
+ * @param {boolean} [props.noWarnings=false] - Flag to disable warnings.
20
+ * @param {string} [props.sdk] - The SDK to upgrade.
21
+ * @param {string} [props.toVersion] - The target version of the SDK.
22
+ * @param {boolean} [props.yolo=false] - Flag to enable YOLO mode.
23
+ *
24
+ * @returns {JSX.Element} The rendered component.
25
+ */
26
+ export default function App(props) {
27
+ const {
28
+ noWarnings = false,
29
+ disableTelemetry = false
30
+ } = props;
31
+ const {
32
+ exit
33
+ } = useApp();
34
+ const [yolo, setYolo] = useState(props.yolo ?? false);
35
+ const [sdks, setSdks] = useState(props.sdk ? [props.sdk] : []);
22
36
  const [sdkGuesses, setSdkGuesses] = useState([]);
23
37
  const [sdkGuessConfirmed, setSdkGuessConfirmed] = useState(false);
24
38
  const [sdkGuessAttempted, setSdkGuessAttempted] = useState(false);
25
- // See comments below, can be enabled on next major
26
- // eslint-disable-next-line no-unused-vars
27
- const [fromVersion, setFromVersion] = useState(_fromVersion);
39
+ const [fromVersion, setFromVersion] = useState(props.fromVersion);
28
40
  const [fromVersionGuessAttempted, setFromVersionGuessAttempted] = useState(false);
29
- // eslint-disable-next-line no-unused-vars
30
- const [toVersion, setToVersion] = useState(_toVersion);
31
- const [dir, setDir] = useState(_dir);
32
- const [ignore, setIgnore] = useState(_ignore);
41
+ const [toVersion, setToVersion] = useState(props.toVersion);
42
+ const [dir, setDir] = useState(props.dir);
43
+ const [ignore, setIgnore] = useState(props.ignore ?? []);
33
44
  const [configComplete, setConfigComplete] = useState(false);
34
45
  const [configVerified, setConfigVerified] = useState(false);
35
46
  const [uuid, setUuid] = useState();
@@ -38,6 +49,24 @@ export default function App({
38
49
  setSdks(SDKS.map(s => s.value));
39
50
  setYolo(false);
40
51
  }
52
+ useEffect(() => {
53
+ if (toVersion === 'core-2') {
54
+ setFromVersion('core-1');
55
+ }
56
+ }, [toVersion]);
57
+ useEffect(() => {
58
+ if (fromVersion === 'core-1') {
59
+ setToVersion('core-2');
60
+ }
61
+ }, [fromVersion]);
62
+
63
+ // Handle the individual SDK upgrade
64
+ if (!fromVersion && !toVersion && sdks[0] === 'nextjs') {
65
+ return /*#__PURE__*/React.createElement(SDKWorkflow, {
66
+ packageManager: props.packageManager,
67
+ sdk: sdks[0]
68
+ });
69
+ }
41
70
 
42
71
  // We try to guess which SDK they are using
43
72
  if (isEmpty(sdks) && isEmpty(sdkGuesses) && !sdkGuessAttempted) {
@@ -48,17 +77,13 @@ export default function App({
48
77
  guesses,
49
78
  _uuid
50
79
  } = guessFrameworks(dir, disableTelemetry);
51
- console.log({
52
- guesses,
53
- _uuid
54
- });
55
80
  setUuid(_uuid);
56
81
  setSdkGuesses(guesses);
57
82
  setSdkGuessAttempted(true);
58
83
  }
59
84
 
60
85
  // We try to guess which version of Clerk they are using
61
- if (!fromVersion && !fromVersionGuess && !fromVersionGuessAttempted) {
86
+ if (isEmpty(sdks) && !fromVersion && !fromVersionGuess && !fromVersionGuessAttempted) {
62
87
  fromVersionGuess = getClerkMajorVersion();
63
88
  setFromVersionGuessAttempted(true);
64
89
  }
@@ -76,14 +101,9 @@ export default function App({
76
101
  color: "red"
77
102
  }, "You are already on version ", toVersion, ", so there's no need to migrate!");
78
103
  }
79
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Gradient, {
80
- name: "vice"
81
- }, /*#__PURE__*/React.createElement(BigText, {
82
- text: "Clerk Upgrade",
83
- font: "tiny"
84
- })), !configComplete && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, /*#__PURE__*/React.createElement(Text, {
104
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Header, null), !configComplete && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, /*#__PURE__*/React.createElement(Text, {
85
105
  color: "blue"
86
- }, "Hello friend!"), " We're excited to help you upgrade Clerk", fromVersion ? ` from ${fromVersion}` : '', toVersion ? ` to ${toVersion}` : '', ". Before we get started, a couple questions..."), /*#__PURE__*/React.createElement(Newline, null)), isEmpty(sdks) && !isEmpty(sdkGuesses) && !sdkGuessConfirmed && /*#__PURE__*/React.createElement(React.Fragment, null, sdkGuesses.length > 1 ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, "It looks like you are using the following Clerk SDKs in your project:"), sdkGuesses.map(guess => /*#__PURE__*/React.createElement(Text, {
106
+ }, "Hello friend!"), " We're excited to help you upgrade Clerk modules. Before we get started, a couple questions..."), /*#__PURE__*/React.createElement(Newline, null)), isEmpty(sdks) && !isEmpty(sdkGuesses) && !sdkGuessConfirmed && /*#__PURE__*/React.createElement(React.Fragment, null, sdkGuesses.length > 1 ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, "It looks like you are using the following Clerk SDKs in your project:"), sdkGuesses.map(guess => /*#__PURE__*/React.createElement(Text, {
87
107
  key: guess.value
88
108
  }, ' ', "- ", guess.label)), /*#__PURE__*/React.createElement(Text, null, "Is that right?")) : /*#__PURE__*/React.createElement(Text, null, "It looks like you are using the \"", sdkGuesses[0].label, "\" Clerk SDK in your project. Is that right?"), /*#__PURE__*/React.createElement(Select, {
89
109
  options: [{
@@ -98,6 +118,8 @@ export default function App({
98
118
  // if true, we were right so we set the sdk
99
119
  if (item === 'yes') {
100
120
  setSdks(sdkGuesses.map(guess => guess.value));
121
+ } else {
122
+ setSdkGuesses([]);
101
123
  }
102
124
  }
103
125
  })), isEmpty(sdks) && isEmpty(sdkGuesses) && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, "Please select which Clerk SDK(s) you're using for your app:"), /*#__PURE__*/React.createElement(Text, {
@@ -106,7 +128,7 @@ export default function App({
106
128
  options: SDKS,
107
129
  onSubmit: value => setSdks(value),
108
130
  visibleOptionCount: SDKS.length
109
- })), !isEmpty(sdks) > 0 && fromVersion && toVersion && !dir && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, "Where would you like for us to scan for files in your project?"), /*#__PURE__*/React.createElement(Text, {
131
+ })), !isEmpty(sdks) && fromVersion && toVersion && !dir && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, "Where would you like for us to scan for files in your project?"), /*#__PURE__*/React.createElement(Text, {
110
132
  color: "gray"
111
133
  }, "(globstar syntax supported)"), /*#__PURE__*/React.createElement(TextInput, {
112
134
  defaultValue: "**/*",
@@ -133,14 +155,14 @@ export default function App({
133
155
  }, " ", ignore.join(', ')))), /*#__PURE__*/React.createElement(Newline, null), /*#__PURE__*/React.createElement(Text, null, "Does this look right?"), /*#__PURE__*/React.createElement(Select, {
134
156
  options: [{
135
157
  label: 'yes',
136
- value: true
158
+ value: 'yes'
137
159
  }, {
138
- label: 'no - exit, and i will try again',
139
- value: false
160
+ label: 'no - exit, and I will try again',
161
+ value: 'no'
140
162
  }],
141
163
  onChange: value => {
142
- if (!value) {
143
- process.exit();
164
+ if (!value || value === 'no') {
165
+ exit();
144
166
  } else {
145
167
  setConfigVerified(true);
146
168
  }
package/dist/cli.js CHANGED
@@ -5,31 +5,31 @@ import React from 'react';
5
5
  import App from './app.js';
6
6
  import sdks from './constants/sdks.js';
7
7
  const cli = meow(`
8
- Usage
9
- $ clerk-upgrade
8
+ Usage
9
+ $ clerk-upgrade
10
10
 
11
- Options
12
- --from Major version number you're upgrading from
13
- --to Major version number you're upgrading to
14
- --sdk Name of the SDK you're upgrading
15
- --dir Directory you'd like to scan for files
16
- --ignore Any files or directories you'd like to ignore
17
- --noWarnings Do not print warnings, only items that must be fixed
18
- --disableTelemetry Do not send anonymous usage telemetry
11
+ Options
12
+ --from Major version number you're upgrading from
13
+ --to Major version number you're upgrading to
14
+ --sdk Name of the SDK you're upgrading
15
+ --dir Directory you'd like to scan for files
16
+ --ignore Any files or directories you'd like to ignore
17
+ --packageManager The package manager you're using (npm, yarn, pnpm)
18
+ --noWarnings Do not print warnings, only items that must be fixed
19
+ --disableTelemetry Do not send anonymous usage telemetry
19
20
 
20
- Examples
21
- $ clerk-upgrade --sdk=nextjs --dir=src/**
21
+ Examples
22
+ $ clerk-upgrade --sdk=nextjs --dir=src/**
22
23
  $ clerk-upgrade --ignore=**/public/** --ignore=**/dist/**
24
+ $ clerk-upgrade --from=core-1 --to=core-2
23
25
  `, {
24
26
  importMeta: import.meta,
25
27
  flags: {
26
28
  from: {
27
- type: 'string',
28
- default: 'core-1'
29
+ type: 'string'
29
30
  },
30
31
  to: {
31
- type: 'string',
32
- default: 'core-2'
32
+ type: 'string'
33
33
  },
34
34
  sdk: {
35
35
  type: 'string',
@@ -42,6 +42,9 @@ const cli = meow(`
42
42
  type: 'string',
43
43
  isMultiple: true
44
44
  },
45
+ packageManager: {
46
+ type: 'string'
47
+ },
45
48
  yolo: {
46
49
  type: 'boolean'
47
50
  },
@@ -53,15 +56,16 @@ const cli = meow(`
53
56
  }
54
57
  }
55
58
  });
56
- render( /*#__PURE__*/React.createElement(App, {
57
- _fromVersion: cli.flags.from,
58
- _toVersion: cli.flags.to,
59
- _sdk: cli.flags.sdk,
60
- _dir: cli.flags.dir,
61
- _ignore: cli.flags.ignore,
62
- _yolo: cli.flags.yolo,
59
+ render(/*#__PURE__*/React.createElement(App, {
60
+ dir: cli.flags.dir,
61
+ disableTelemetry: cli.flags.disableTelemetry,
62
+ fromVersion: cli.flags.from,
63
+ ignore: cli.flags.ignore,
63
64
  noWarnings: cli.flags.noWarnings,
64
- disableTelemetry: cli.flags.disableTelemetry
65
+ packageManager: cli.flags.packageManager,
66
+ sdk: cli.flags.sdk,
67
+ toVersion: cli.flags.to,
68
+ yolo: cli.flags.yolo
65
69
  })
66
70
  // if having issues with errors being swallowed, uncomment this
67
71
  // { debug: true },
@@ -0,0 +1,216 @@
1
+ export const fixtures = [{
2
+ name: 'Basic await transform',
3
+ source: `
4
+ import { auth } from '@clerk/nextjs/server';
5
+
6
+ export function any() {
7
+ const { userId } = auth();
8
+ return new Response(JSON.stringify({ userId }));
9
+ }
10
+
11
+ export function another() {
12
+ return true;
13
+ }
14
+ `,
15
+ output: `
16
+ import { auth } from '@clerk/nextjs/server';
17
+
18
+ export async function any() {
19
+ const { userId } = await auth();
20
+ return new Response(JSON.stringify({ userId }));
21
+ }
22
+
23
+ export function another() {
24
+ return true;
25
+ }
26
+ `
27
+ }, {
28
+ name: 'Arrow function await transform',
29
+ source: `
30
+ 'use server';
31
+
32
+ import { patchCommunication, postSMSTemplate } from '@/app/api/instances';
33
+ import { Instance } from '@/app/api/types/instances';
34
+ import { Template } from '@/app/api/types/templates';
35
+ import { auth } from '@clerk/nextjs/server';
36
+
37
+ export const toggleSMSTemplate = async (
38
+ instanceId: string,
39
+ template: Template,
40
+ ) => {
41
+ const { getToken } = auth();
42
+
43
+ return await postSMSTemplate(instanceId, template, {
44
+ token: await getToken(),
45
+ });
46
+ };`,
47
+ output: `
48
+ 'use server';
49
+
50
+ import { patchCommunication, postSMSTemplate } from '@/app/api/instances';
51
+ import { Instance } from '@/app/api/types/instances';
52
+ import { Template } from '@/app/api/types/templates';
53
+ import { auth } from '@clerk/nextjs/server';
54
+
55
+ export const toggleSMSTemplate = async (
56
+ instanceId: string,
57
+ template: Template,
58
+ ) => {
59
+ const { getToken } = await auth();
60
+
61
+ return await postSMSTemplate(instanceId, template, {
62
+ token: await getToken(),
63
+ });
64
+ };`
65
+ }, {
66
+ name: 'auth().protect -> await auth.protect()',
67
+ source: `
68
+ import { auth } from '@clerk/nextjs/server';
69
+
70
+ export function GET() {
71
+ const { userId } = auth().protect(
72
+ (has) => has({ role: 'admin' }) || has({ role: 'org:editor' }),
73
+ );
74
+ return new Response(JSON.stringify({ userId }));
75
+ }
76
+ `,
77
+ output: `
78
+ import { auth } from '@clerk/nextjs/server';
79
+
80
+ export async function GET() {
81
+ const { userId } = await auth.protect(
82
+ (has) => has({ role: 'admin' }) || has({ role: 'org:editor' }),
83
+ );
84
+ return new Response(JSON.stringify({ userId }));
85
+ }
86
+ `
87
+ }, {
88
+ name: 'Basic clerkMiddleware()',
89
+ source: `
90
+ import {
91
+ clerkMiddleware,
92
+ createRouteMatcher
93
+ } from "@clerk/nextjs/server"
94
+
95
+ const isPublicRoute = createRouteMatcher(["/", "/contact"])
96
+
97
+ export default clerkMiddleware((auth, req) => {
98
+ auth().protect(); // for any other route, require auth
99
+ })
100
+ `,
101
+ output: `
102
+ import {
103
+ clerkMiddleware,
104
+ createRouteMatcher
105
+ } from "@clerk/nextjs/server"
106
+
107
+ const isPublicRoute = createRouteMatcher(["/", "/contact"])
108
+
109
+ export default clerkMiddleware(async (auth, req) => {
110
+ await auth.protect(); // for any other route, require auth
111
+ })
112
+ `
113
+ }, {
114
+ name: 'Complex clerkMiddleware()',
115
+ source: `
116
+ import {
117
+ clerkMiddleware,
118
+ createRouteMatcher
119
+ } from "@clerk/nextjs/server"
120
+ import createMiddleware from "next-intl/middleware"
121
+
122
+ const intlMiddleware = createMiddleware({
123
+ locales: ["en", "de"],
124
+ defaultLocale: "en",
125
+ })
126
+
127
+ const isDashboardRoute = createRouteMatcher(["/dashboard(.*)"])
128
+
129
+ export default clerkMiddleware((auth, request) => {
130
+ if (isDashboardRoute(request)) auth().protect()
131
+
132
+ return intlMiddleware(request)
133
+ })
134
+ `,
135
+ output: `
136
+ import {
137
+ clerkMiddleware,
138
+ createRouteMatcher
139
+ } from "@clerk/nextjs/server"
140
+ import createMiddleware from "next-intl/middleware"
141
+
142
+ const intlMiddleware = createMiddleware({
143
+ locales: ["en", "de"],
144
+ defaultLocale: "en",
145
+ })
146
+
147
+ const isDashboardRoute = createRouteMatcher(["/dashboard(.*)"])
148
+
149
+ export default clerkMiddleware(async (auth, request) => {
150
+ if (isDashboardRoute(request)) await auth.protect()
151
+
152
+ return intlMiddleware(request)
153
+ })
154
+ `
155
+ }, {
156
+ name: 'Complex clerkMiddleware() with protect being destructured from auth()',
157
+ source: `
158
+ import { clerkMiddleware } from '@clerk/nextjs/server';
159
+
160
+ export default clerkMiddleware(
161
+ (auth, req) => {
162
+ const { protect, sessionClaims } = auth();
163
+
164
+ protect();
165
+ },
166
+ );
167
+ `,
168
+ output: `
169
+ import { clerkMiddleware } from '@clerk/nextjs/server';
170
+
171
+ export default clerkMiddleware(
172
+ async (auth, req) => {
173
+ const {
174
+ sessionClaims
175
+ } = await auth();
176
+
177
+ await auth.protect();
178
+ },
179
+ );
180
+ `
181
+ }, {
182
+ name: 'Complex clerkMiddleware() without protect()',
183
+ source: `
184
+ import { clerkMiddleware } from '@clerk/nextjs/server';
185
+
186
+ export default clerkMiddleware(
187
+ (auth, req) => {
188
+ const { redirectToSignIn } = auth();
189
+
190
+ redirectToSignIn();
191
+ },
192
+ );
193
+ `,
194
+ output: `
195
+ import { clerkMiddleware } from '@clerk/nextjs/server';
196
+
197
+ export default clerkMiddleware(
198
+ async (auth, req) => {
199
+ const { redirectToSignIn } = await auth();
200
+
201
+ redirectToSignIn();
202
+ },
203
+ );
204
+ `
205
+ }, {
206
+ name: 'Does not transform other imports',
207
+ source: `
208
+ import { auth } from '@some/other/module';
209
+
210
+ export function any() {
211
+ const { IBauthed } = auth();
212
+ return new Response(JSON.stringify({ IBauthed }));
213
+ }
214
+ `,
215
+ output: ''
216
+ }];
@@ -0,0 +1,15 @@
1
+ import { applyTransform } from 'jscodeshift/dist/testUtils';
2
+ import { describe, expect, it } from 'vitest';
3
+ import transformer from '../transform-async-request.cjs';
4
+ import { fixtures } from './__fixtures__/transform-async-request.fixtures';
5
+ describe('transform-async-request', () => {
6
+ it.each(fixtures)(`$name`, ({
7
+ source,
8
+ output
9
+ }) => {
10
+ const result = applyTransform(transformer, {}, {
11
+ source
12
+ });
13
+ expect(result).toEqual(output.trim());
14
+ });
15
+ });
@@ -0,0 +1,25 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { globby } from 'globby';
4
+ import { run } from 'jscodeshift/src/Runner.js';
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ export async function runCodemod(transform, glob, options) {
7
+ if (!transform) {
8
+ throw new Error('No transform provided');
9
+ }
10
+ const resolvedPath = resolve(__dirname, `${transform}.cjs`);
11
+ const paths = await globby(glob, {
12
+ 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)+',
13
+ // common image files
14
+ '**/*.(mp4|mkv|wmv|m4v|mov|avi|flv|webm|flac|mka|m4a|aac|ogg)+',
15
+ // common video files] }).then(files => {
16
+ '**/*.(css|scss|sass|less|styl)+' // common style files
17
+ ]
18
+ });
19
+ return await run(resolvedPath, paths ?? [], {
20
+ dry: false,
21
+ ...options,
22
+ // we must silence stdout to prevent output from interfering with ink CLI
23
+ silent: true
24
+ });
25
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Transforms the source code by modifying calls to `auth().protect()` and ensuring
3
+ * that calls to `auth` are awaited within function declarations.
4
+ *
5
+ * @param {import('jscodeshift').FileInfo} FileInfo - The parameters object
6
+ * @param {import('jscodeshift').API} api - The API object provided by jscodeshift
7
+ * @param {Object} _options - Additional options (unused)
8
+ * @returns {string|undefined} - The transformed source code if modifications were made, otherwise undefined
9
+ */
10
+ module.exports = function transformAsyncRequest({
11
+ path,
12
+ source
13
+ }, {
14
+ jscodeshift: j
15
+ }, _options) {
16
+ const root = j(source);
17
+ let dirtyFlag = false;
18
+
19
+ // Short-circuit if the import from '@clerk/nextjs/server' is not found
20
+ if (root.find(j.ImportDeclaration, {
21
+ source: {
22
+ value: '@clerk/nextjs/server'
23
+ }
24
+ }).size() === 0) {
25
+ return undefined;
26
+ }
27
+
28
+ // Helper function to make a function async
29
+ function makeFunctionAsync(func) {
30
+ if (!func.async) {
31
+ func.async = true;
32
+ dirtyFlag = true;
33
+ }
34
+ }
35
+ function transformFunctions(func) {
36
+ let authCallFound = false;
37
+ let authProtectFound = false;
38
+
39
+ // Find the auth().protect call and transform it
40
+ j(func).find(j.CallExpression, {
41
+ callee: {
42
+ type: 'MemberExpression',
43
+ object: {
44
+ type: 'CallExpression',
45
+ callee: {
46
+ name: 'auth'
47
+ }
48
+ },
49
+ property: {
50
+ name: 'protect'
51
+ }
52
+ }
53
+ }).forEach(authProtectPath => {
54
+ // Replace auth().protect with await auth.protect
55
+ authProtectPath.node.callee = j.memberExpression(j.identifier('auth'), j.identifier('protect'));
56
+ authProtectPath.replace(j.awaitExpression(authProtectPath.node));
57
+ authProtectFound = true;
58
+ dirtyFlag = true;
59
+ });
60
+
61
+ // Find the auth() call and transform it
62
+ j(func).find(j.CallExpression, {
63
+ callee: {
64
+ name: 'auth'
65
+ }
66
+ }).forEach(authPath => {
67
+ authCallFound = true;
68
+ // Ensure the call to 'auth' is awaited
69
+ if (!j.AwaitExpression.check(authPath.parent.node)) {
70
+ j(authPath).replaceWith(j.awaitExpression(authPath.node));
71
+ dirtyFlag = true;
72
+ }
73
+ });
74
+
75
+ // Add 'async' keyword to the function declaration
76
+ if (!func.node.async && (authCallFound || authProtectFound)) {
77
+ func.node.async = true;
78
+ dirtyFlag = true;
79
+ }
80
+ }
81
+
82
+ // Find all function expressions
83
+ root.find(j.FunctionDeclaration).forEach(transformFunctions);
84
+ root.find(j.ArrowFunctionExpression).forEach(transformFunctions);
85
+
86
+ // Find the default export which is a call to clerkMiddleware
87
+ root.find(j.ExportDefaultDeclaration).forEach(path => {
88
+ const declaration = path.node.declaration;
89
+ if (j.CallExpression.check(declaration) && j.Identifier.check(declaration.callee) && declaration.callee.name === 'clerkMiddleware') {
90
+ const middlewareFunction = declaration.arguments[0];
91
+ if (j.FunctionExpression.check(middlewareFunction) || j.ArrowFunctionExpression.check(middlewareFunction)) {
92
+ // Add async keyword to the function
93
+ makeFunctionAsync(middlewareFunction);
94
+
95
+ // Find the destructuring assignment and modify it
96
+ j(middlewareFunction.body).find(j.VariableDeclarator).forEach(varPath => {
97
+ const id = varPath.node.id;
98
+ const init = varPath.node.init;
99
+ if ((j.CallExpression.check(init) || j.AwaitExpression.check(init)) && (init.callee?.name === 'auth' || init.argument?.callee?.name === 'auth')) {
100
+ // Remove 'protect' from destructuring
101
+ id.properties = id.properties.filter(prop => {
102
+ return !(j.Identifier.check(prop.key) && prop.key.name === 'protect');
103
+ });
104
+ dirtyFlag = true;
105
+ }
106
+ });
107
+
108
+ // Replace protect() call with await auth.protect()
109
+ j(middlewareFunction.body).find(j.ExpressionStatement).forEach(exprPath => {
110
+ const expr = exprPath.node.expression;
111
+ if (j.CallExpression.check(expr) && j.Identifier.check(expr.callee) && expr.callee.name === 'protect') {
112
+ exprPath.replace(j.expressionStatement(j.awaitExpression(j.callExpression(j.memberExpression(j.identifier('auth'), j.identifier('protect')), []))));
113
+ dirtyFlag = true;
114
+ }
115
+ });
116
+ }
117
+ }
118
+ });
119
+ return dirtyFlag ? root.toSource() : undefined;
120
+ };
121
+ module.exports.parser = 'tsx';
@@ -0,0 +1,66 @@
1
+ import { Spinner, StatusMessage, TextInput } from '@inkjs/ui';
2
+ import { Newline, Text } from 'ink';
3
+ import React, { useEffect, useState } from 'react';
4
+ import { runCodemod } from '../codemods/index.js';
5
+
6
+ /**
7
+ * Codemod component that allows users to run a codemod transformation on their project files.
8
+ *
9
+ * @param {Object} props
10
+ * @param {Function} props.callback - The callback function to be called after the codemod is run.
11
+ * @param {string} props.glob - The directory to scan for files in the project.
12
+ * @param {string} props.sdk - The SDK name to be used in the codemod.
13
+ * @param {string} props.transform - The transformation to be applied by the codemod.
14
+ *
15
+ * @returns {JSX.Element} The rendered Codemod component.
16
+ */
17
+ export function Codemod(props) {
18
+ const {
19
+ callback,
20
+ sdk,
21
+ transform
22
+ } = props;
23
+ const [error, setError] = useState();
24
+ const [glob, setGlob] = useState(props.glob);
25
+ const [result, setResult] = useState();
26
+ useEffect(() => {
27
+ if (!glob) {
28
+ return;
29
+ }
30
+ runCodemod(transform, glob).then(res => {
31
+ setResult(res);
32
+ }).catch(err => {
33
+ setError(err);
34
+ }).finally(() => {
35
+ callback(true);
36
+ });
37
+ }, [glob]);
38
+ return /*#__PURE__*/React.createElement(React.Fragment, null, glob ? /*#__PURE__*/React.createElement(StatusMessage, {
39
+ variant: "success"
40
+ }, "Scanning for files in your project... ", glob.toString()) : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, "Where would you like for us to scan for files in your project?"), /*#__PURE__*/React.createElement(Text, {
41
+ color: "gray"
42
+ }, "(globstar syntax supported)"), glob ? /*#__PURE__*/React.createElement(Text, null, glob.toString()) : /*#__PURE__*/React.createElement(TextInput, {
43
+ defaultValue: "**/*",
44
+ onSubmit: val => {
45
+ setGlob(val.split(/[ ,]/));
46
+ }
47
+ })), !result && !error && glob && /*#__PURE__*/React.createElement(Spinner, {
48
+ label: `Running @clerk/${sdk}</Text> codemod... ${transform}`
49
+ }), result && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StatusMessage, {
50
+ variant: "success"
51
+ }, "Running ", /*#__PURE__*/React.createElement(Text, {
52
+ bold: true
53
+ }, "@clerk/", sdk), " codemod... ", transform, " complete!"), /*#__PURE__*/React.createElement(Newline, null), /*#__PURE__*/React.createElement(Text, {
54
+ bold: true
55
+ }, "Codemod results:"), /*#__PURE__*/React.createElement(Text, {
56
+ color: "red"
57
+ }, result.error ?? 0, " errors"), /*#__PURE__*/React.createElement(Text, {
58
+ color: "green"
59
+ }, result.ok ?? 0, " ok"), /*#__PURE__*/React.createElement(Text, {
60
+ color: "yellow"
61
+ }, result.skip ?? 0, " skipped"), /*#__PURE__*/React.createElement(Text, {
62
+ color: "gray"
63
+ }, result.nochange ?? 0, " unmodified"), result.timeElapsed && /*#__PURE__*/React.createElement(Text, null, "Time elapsed: ", result.timeElapsed), /*#__PURE__*/React.createElement(Newline, null)), error && /*#__PURE__*/React.createElement(Text, {
64
+ color: "red"
65
+ }, error.message));
66
+ }
@@ -0,0 +1,11 @@
1
+ import BigText from 'ink-big-text';
2
+ import Gradient from 'ink-gradient';
3
+ import React from 'react';
4
+ export function Header() {
5
+ return /*#__PURE__*/React.createElement(Gradient, {
6
+ name: "vice"
7
+ }, /*#__PURE__*/React.createElement(BigText, {
8
+ text: "Clerk Upgrade",
9
+ font: "tiny"
10
+ }));
11
+ }
@@ -0,0 +1,114 @@
1
+ import { Select, Spinner, StatusMessage } from '@inkjs/ui';
2
+ import { execa } from 'execa';
3
+ import { Text } from 'ink';
4
+ import React, { useEffect, useState } from 'react';
5
+ import { getUpgradeCommand } from '../util/detect-package-manager.js';
6
+ import { getClerkSdkVersion } from '../util/get-clerk-version.js';
7
+ import { Codemod } from './Codemod.js';
8
+ import { Header } from './Header.js';
9
+
10
+ /**
11
+ * SDKWorkflow component handles the upgrade process for a given SDK.
12
+ * It checks the current version of the SDK and provides the necessary steps
13
+ * to upgrade or run codemods based on the version.
14
+ *
15
+ * @component
16
+ * @param {Object} props
17
+ * @param {string} props.packageManager - The package manager to use for the upgrade, if needed.
18
+ * @param {string} props.sdk - The SDK to be upgraded.
19
+ *
20
+ * @returns {JSX.Element} The rendered component.
21
+ */
22
+ export function SDKWorkflow(props) {
23
+ const {
24
+ packageManager,
25
+ sdk
26
+ } = props;
27
+ const [done, setDone] = useState(false);
28
+ const [upgradeComplete, setUpgradeComplete] = useState(false);
29
+ const version = getClerkSdkVersion(sdk);
30
+ if (sdk !== 'nextjs') {
31
+ return /*#__PURE__*/React.createElement(StatusMessage, {
32
+ variant: "error"
33
+ }, "The SDK upgrade functionality is only available for ", /*#__PURE__*/React.createElement(Text, {
34
+ bold: true
35
+ }, "@clerk/nextjs"), " at the moment.");
36
+ }
37
+
38
+ // Right now, we only have one codemod for the async request transformation
39
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Header, null), version === 5 && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(UpgradeCommand, {
40
+ callback: setUpgradeComplete,
41
+ packageManager: packageManager,
42
+ sdk: sdk
43
+ }), upgradeComplete ? /*#__PURE__*/React.createElement(Codemod, {
44
+ callback: setDone,
45
+ sdk: sdk,
46
+ transform: "transform-async-request"
47
+ }) : null), !done && version === 6 && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, "Looks like you are already on the latest version of ", /*#__PURE__*/React.createElement(Text, {
48
+ bold: true
49
+ }, "@clerk/", sdk), ". Would you like to run the associated codemod?."), upgradeComplete ? /*#__PURE__*/React.createElement(Codemod, {
50
+ sdk: sdk,
51
+ callback: setDone
52
+ }) : /*#__PURE__*/React.createElement(Select, {
53
+ options: [{
54
+ label: 'yes',
55
+ value: 'yes'
56
+ }, {
57
+ label: 'no',
58
+ value: 'no'
59
+ }],
60
+ onChange: value => {
61
+ if (value === 'yes') {
62
+ setUpgradeComplete(true);
63
+ } else {
64
+ setDone(true);
65
+ }
66
+ }
67
+ })), done && /*#__PURE__*/React.createElement(StatusMessage, {
68
+ variant: "success"
69
+ }, "Done upgrading ", /*#__PURE__*/React.createElement(Text, {
70
+ bold: true
71
+ }, "@clerk/nextjs")));
72
+ }
73
+
74
+ /**
75
+ * Component that runs an upgrade command for a given SDK and handles the result.
76
+ *
77
+ * @component
78
+ * @param {Object} props
79
+ * @param {Function} props.callback - The callback function to be called after the command execution.
80
+ * @param {string} props.sdk - The SDK for which the upgrade command is run.
81
+ * @returns {JSX.Element} The rendered component.
82
+ *
83
+ * @example
84
+ * <UpgradeCommand sdk="example-sdk" callback={handleUpgrade} />
85
+ */
86
+ function UpgradeCommand({
87
+ callback,
88
+ sdk
89
+ }) {
90
+ const [error, setError] = useState();
91
+ const [result, setResult] = useState();
92
+ const command = getUpgradeCommand(sdk);
93
+ useEffect(() => {
94
+ execa({
95
+ shell: true
96
+ })`${command}`.then(res => {
97
+ setResult(res);
98
+ callback(true);
99
+ }).catch(err => {
100
+ setError(err);
101
+ });
102
+ }, [command]);
103
+ return /*#__PURE__*/React.createElement(React.Fragment, null, !result && !error && /*#__PURE__*/React.createElement(Spinner, {
104
+ label: `Running upgrade command: ${command}`
105
+ }), result && /*#__PURE__*/React.createElement(StatusMessage, {
106
+ variant: "success"
107
+ }, /*#__PURE__*/React.createElement(Text, {
108
+ bold: true
109
+ }, "@clerk/", sdk), " upgraded successfully to ", /*#__PURE__*/React.createElement(Text, {
110
+ bold: true
111
+ }, "latest!")), error && /*#__PURE__*/React.createElement(StatusMessage, {
112
+ variant: "error"
113
+ }, "Upgrade failed!"));
114
+ }
@@ -5,17 +5,18 @@ import indexToPosition from 'index-to-position';
5
5
  import { Newline, Text } from 'ink';
6
6
  import path from 'path';
7
7
  import React, { useEffect, useState } from 'react';
8
- import ExpandableList from './util/expandable-list.js';
9
- export default function Scan({
10
- fromVersion,
11
- toVersion,
12
- sdks,
13
- dir,
14
- ignore,
15
- noWarnings,
16
- uuid,
17
- disableTelemetry
18
- }) {
8
+ import ExpandableList from '../util/expandable-list.js';
9
+ export function Scan(props) {
10
+ const {
11
+ fromVersion,
12
+ toVersion,
13
+ sdks,
14
+ dir,
15
+ ignore,
16
+ noWarnings,
17
+ uuid,
18
+ disableTelemetry
19
+ } = props;
19
20
  // NOTE: if the difference between fromVersion and toVersion is greater than 1
20
21
  // we need to do a little extra work here and import two matchers,
21
22
  // sequence them after each other, and clearly mark which version migration
@@ -36,7 +37,7 @@ export default function Scan({
36
37
  // { sdkName: [{ title: 'x', matcher: /x/, slug: 'x', ... }] }
37
38
  useEffect(() => {
38
39
  setStatus(`Loading data for ${toVersion} migration`);
39
- import(`./versions/${toVersion}/index.js`).then(version => {
40
+ import(`../versions/${toVersion}/index.js`).then(version => {
40
41
  setMatchers(sdks.reduce((m, sdk) => {
41
42
  m[sdk] = version.default[sdk];
42
43
  return m;
@@ -49,12 +50,9 @@ export default function Scan({
49
50
  // result = `files` set to format: ['/filename', '/other/filename']
50
51
  useEffect(() => {
51
52
  setStatus('Collecting files to scan');
52
- ignore.push('node_modules/**', '**/node_modules/**', '.git/**', '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)+',
53
- // common image files
54
- '**/*.(mp4|mkv|wmv|m4v|mov|avi|flv|webm|flac|mka|m4a|aac|ogg)+' // common video files
55
- );
56
- globby(convertPathToPattern(path.resolve(dir)), {
57
- ignore: ignore.filter(Boolean)
53
+ const pattern = convertPathToPattern(path.resolve(dir));
54
+ globby(pattern, {
55
+ ignore: ['node_modules/**', '**/node_modules/**', '.git/**', '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)+', ...ignore].filter(Boolean)
58
56
  }).then(files => {
59
57
  setFiles(files);
60
58
  });
@@ -0,0 +1,22 @@
1
+ import { existsSync } from 'fs';
2
+ export function detectPackageManager() {
3
+ if (existsSync('package-lock.json')) {
4
+ return 'npm';
5
+ } else if (existsSync('yarn.lock')) {
6
+ return 'yarn';
7
+ } else if (existsSync('pnpm-lock.yaml')) {
8
+ return 'pnpm';
9
+ } else {
10
+ return 'npm';
11
+ }
12
+ }
13
+ export function getUpgradeCommand(sdk, packageManager) {
14
+ switch (packageManager || detectPackageManager()) {
15
+ case 'yarn':
16
+ return `yarn add @clerk/${sdk}@latest`;
17
+ case 'pnpm':
18
+ return `pnpm add @clerk/${sdk}@latest`;
19
+ default:
20
+ return `npm install @clerk/${sdk}@latest`;
21
+ }
22
+ }
@@ -74,7 +74,7 @@ export default function ExpandableList({
74
74
  bottomRight: ' ',
75
75
  right: ''
76
76
  };
77
- memo.push( /*#__PURE__*/React.createElement(Box, {
77
+ memo.push(/*#__PURE__*/React.createElement(Box, {
78
78
  borderStyle: item.focused ? doubleBorderStyle : singleBorderStyle,
79
79
  flexDirection: "column",
80
80
  borderColor: item.focused ? 'blue' : 'white',
@@ -1,7 +1,27 @@
1
1
  import { readPackageSync } from 'read-pkg';
2
2
  import semverRegex from 'semver-regex';
3
- export default function getClerkMajorVersion() {
3
+ export function getClerkMajorVersion() {
4
4
  const pkg = readPackageSync();
5
5
  const clerk = pkg.dependencies.clerk;
6
- return clerk ? semverRegex.exec(clerk)[0][0] : false;
6
+ return clerk ? semverRegex().exec(clerk)[0][0] : false;
7
+ }
8
+ export function getClerkSdkVersion(sdk) {
9
+ const pkg = readPackageSync();
10
+ const clerkSdk = pkg.dependencies[`@clerk/${sdk}`];
11
+ if (!clerkSdk) {
12
+ return false;
13
+ }
14
+ try {
15
+ return getMajorVersion(clerkSdk.replace('^', '').replace('~', ''));
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+ function getMajorVersion(semver) {
21
+ const match = semver.match(semverRegex());
22
+ if (match) {
23
+ const [major] = match[0].split('.');
24
+ return parseInt(major, 10); // Return as an integer
25
+ }
26
+ throw new Error('Invalid semver string');
7
27
  }
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@clerk/upgrade",
3
- "version": "1.0.9",
3
+ "version": "1.1.0-canary.v1997eac",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/clerk/javascript.git",
7
+ "directory": "packages/upgrade"
8
+ },
4
9
  "license": "MIT",
5
10
  "type": "module",
6
11
  "main": "dist/cli.js",
@@ -11,16 +16,12 @@
11
16
  "dist"
12
17
  ],
13
18
  "scripts": {
14
- "build": "npm run clean && NODE_ENV=production babel --out-dir=dist src --copy-files",
19
+ "build": "npm run clean && NODE_ENV=production babel --keep-file-extension --out-dir=dist src --copy-files",
15
20
  "clean": "del-cli dist/*",
16
- "dev": "babel --out-dir=dist --watch src --copy-files",
21
+ "dev": "babel --keep-file-extension --out-dir=dist --watch src --copy-files",
17
22
  "lint": "eslint src/",
18
- "lint:publint": "publint"
19
- },
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/clerk/javascript.git",
23
- "directory": "packages/upgrade"
23
+ "lint:publint": "publint",
24
+ "test": "vitest"
24
25
  },
25
26
  "babel": {
26
27
  "presets": [
@@ -28,16 +29,18 @@
28
29
  ]
29
30
  },
30
31
  "dependencies": {
31
- "@inkjs/ui": "^1.0.0",
32
+ "@inkjs/ui": "^2.0.0",
32
33
  "@jescalan/ink-markdown": "^2.0.0",
33
34
  "ejs": "3.1.10",
35
+ "execa": "9.4.1",
34
36
  "globby": "^14.0.1",
35
37
  "gray-matter": "^4.0.3",
36
38
  "index-to-position": "^0.1.2",
37
- "ink": "^4.4.1",
39
+ "ink": "^5.0.1",
38
40
  "ink-big-text": "^2.0.0",
39
41
  "ink-gradient": "^3.0.0",
40
- "ink-link": "^3.0.0",
42
+ "ink-link": "^4.1.0",
43
+ "jscodeshift": "^17.0.0",
41
44
  "marked": "^11.1.1",
42
45
  "meow": "^11.0.0",
43
46
  "react": "^18.3.1",
@@ -48,9 +51,10 @@
48
51
  "devDependencies": {
49
52
  "@babel/cli": "^7.24.7",
50
53
  "@babel/preset-react": "^7.24.7",
51
- "chalk": "^5.3.0",
54
+ "@types/jscodeshift": "^0.12.0",
52
55
  "del-cli": "^5.1.0",
53
- "eslint-config-custom": "*"
56
+ "eslint-config-custom": "*",
57
+ "vitest": "^2.1.3"
54
58
  },
55
59
  "engines": {
56
60
  "node": ">=18.17.0"