@akinon/projectzero 2.0.0-beta.21 → 2.0.0-beta.22
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# projectzeronext
|
|
2
2
|
|
|
3
|
+
## 2.0.0-beta.22
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @akinon/next@2.0.0-beta.22
|
|
8
|
+
- @akinon/pz-akifast@2.0.0-beta.22
|
|
9
|
+
- @akinon/pz-apple-pay@2.0.0-beta.22
|
|
10
|
+
- @akinon/pz-b2b@2.0.0-beta.22
|
|
11
|
+
- @akinon/pz-basket-gift-pack@2.0.0-beta.22
|
|
12
|
+
- @akinon/pz-bkm@2.0.0-beta.22
|
|
13
|
+
- @akinon/pz-checkout-gift-pack@2.0.0-beta.22
|
|
14
|
+
- @akinon/pz-click-collect@2.0.0-beta.22
|
|
15
|
+
- @akinon/pz-credit-payment@2.0.0-beta.22
|
|
16
|
+
- @akinon/pz-cybersource-uc@2.0.0-beta.22
|
|
17
|
+
- @akinon/pz-flow-payment@2.0.0-beta.22
|
|
18
|
+
- @akinon/pz-google-pay@2.0.0-beta.22
|
|
19
|
+
- @akinon/pz-gpay@2.0.0-beta.22
|
|
20
|
+
- @akinon/pz-haso@2.0.0-beta.22
|
|
21
|
+
- @akinon/pz-hepsipay@2.0.0-beta.22
|
|
22
|
+
- @akinon/pz-masterpass@2.0.0-beta.22
|
|
23
|
+
- @akinon/pz-masterpass-rest@2.0.0-beta.22
|
|
24
|
+
- @akinon/pz-multi-basket@2.0.0-beta.22
|
|
25
|
+
- @akinon/pz-one-click-checkout@2.0.0-beta.22
|
|
26
|
+
- @akinon/pz-otp@2.0.0-beta.22
|
|
27
|
+
- @akinon/pz-pay-on-delivery@2.0.0-beta.22
|
|
28
|
+
- @akinon/pz-saved-card@2.0.0-beta.22
|
|
29
|
+
- @akinon/pz-similar-products@2.0.0-beta.22
|
|
30
|
+
- @akinon/pz-tabby-extension@2.0.0-beta.22
|
|
31
|
+
- @akinon/pz-tamara-extension@2.0.0-beta.22
|
|
32
|
+
- @akinon/pz-theme@2.0.0-beta.22
|
|
33
|
+
- @akinon/pz-virtual-try-on@2.0.0-beta.22
|
|
34
|
+
|
|
3
35
|
## 2.0.0-beta.21
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectzeronext",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.22",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"scripts": {
|
|
@@ -24,33 +24,33 @@
|
|
|
24
24
|
"test:middleware": "jest middleware-matcher.test.ts --bail"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@akinon/next": "2.0.0-beta.
|
|
28
|
-
"@akinon/pz-akifast": "2.0.0-beta.
|
|
29
|
-
"@akinon/pz-apple-pay": "2.0.0-beta.
|
|
30
|
-
"@akinon/pz-b2b": "2.0.0-beta.
|
|
31
|
-
"@akinon/pz-basket-gift-pack": "2.0.0-beta.
|
|
32
|
-
"@akinon/pz-bkm": "2.0.0-beta.
|
|
33
|
-
"@akinon/pz-checkout-gift-pack": "2.0.0-beta.
|
|
34
|
-
"@akinon/pz-click-collect": "2.0.0-beta.
|
|
35
|
-
"@akinon/pz-credit-payment": "2.0.0-beta.
|
|
36
|
-
"@akinon/pz-cybersource-uc": "2.0.0-beta.
|
|
37
|
-
"@akinon/pz-flow-payment": "2.0.0-beta.
|
|
38
|
-
"@akinon/pz-google-pay": "2.0.0-beta.
|
|
39
|
-
"@akinon/pz-gpay": "2.0.0-beta.
|
|
40
|
-
"@akinon/pz-haso": "2.0.0-beta.
|
|
41
|
-
"@akinon/pz-hepsipay": "2.0.0-beta.
|
|
42
|
-
"@akinon/pz-masterpass": "2.0.0-beta.
|
|
43
|
-
"@akinon/pz-masterpass-rest": "2.0.0-beta.
|
|
44
|
-
"@akinon/pz-multi-basket": "2.0.0-beta.
|
|
45
|
-
"@akinon/pz-one-click-checkout": "2.0.0-beta.
|
|
46
|
-
"@akinon/pz-otp": "2.0.0-beta.
|
|
47
|
-
"@akinon/pz-pay-on-delivery": "2.0.0-beta.
|
|
48
|
-
"@akinon/pz-saved-card": "2.0.0-beta.
|
|
49
|
-
"@akinon/pz-similar-products": "2.0.0-beta.
|
|
50
|
-
"@akinon/pz-tabby-extension": "2.0.0-beta.
|
|
51
|
-
"@akinon/pz-tamara-extension": "2.0.0-beta.
|
|
52
|
-
"@akinon/pz-theme": "2.0.0-beta.
|
|
53
|
-
"@akinon/pz-virtual-try-on": "2.0.0-beta.
|
|
27
|
+
"@akinon/next": "2.0.0-beta.22",
|
|
28
|
+
"@akinon/pz-akifast": "2.0.0-beta.22",
|
|
29
|
+
"@akinon/pz-apple-pay": "2.0.0-beta.22",
|
|
30
|
+
"@akinon/pz-b2b": "2.0.0-beta.22",
|
|
31
|
+
"@akinon/pz-basket-gift-pack": "2.0.0-beta.22",
|
|
32
|
+
"@akinon/pz-bkm": "2.0.0-beta.22",
|
|
33
|
+
"@akinon/pz-checkout-gift-pack": "2.0.0-beta.22",
|
|
34
|
+
"@akinon/pz-click-collect": "2.0.0-beta.22",
|
|
35
|
+
"@akinon/pz-credit-payment": "2.0.0-beta.22",
|
|
36
|
+
"@akinon/pz-cybersource-uc": "2.0.0-beta.22",
|
|
37
|
+
"@akinon/pz-flow-payment": "2.0.0-beta.22",
|
|
38
|
+
"@akinon/pz-google-pay": "2.0.0-beta.22",
|
|
39
|
+
"@akinon/pz-gpay": "2.0.0-beta.22",
|
|
40
|
+
"@akinon/pz-haso": "2.0.0-beta.22",
|
|
41
|
+
"@akinon/pz-hepsipay": "2.0.0-beta.22",
|
|
42
|
+
"@akinon/pz-masterpass": "2.0.0-beta.22",
|
|
43
|
+
"@akinon/pz-masterpass-rest": "2.0.0-beta.22",
|
|
44
|
+
"@akinon/pz-multi-basket": "2.0.0-beta.22",
|
|
45
|
+
"@akinon/pz-one-click-checkout": "2.0.0-beta.22",
|
|
46
|
+
"@akinon/pz-otp": "2.0.0-beta.22",
|
|
47
|
+
"@akinon/pz-pay-on-delivery": "2.0.0-beta.22",
|
|
48
|
+
"@akinon/pz-saved-card": "2.0.0-beta.22",
|
|
49
|
+
"@akinon/pz-similar-products": "2.0.0-beta.22",
|
|
50
|
+
"@akinon/pz-tabby-extension": "2.0.0-beta.22",
|
|
51
|
+
"@akinon/pz-tamara-extension": "2.0.0-beta.22",
|
|
52
|
+
"@akinon/pz-theme": "2.0.0-beta.22",
|
|
53
|
+
"@akinon/pz-virtual-try-on": "2.0.0-beta.22",
|
|
54
54
|
"@hookform/resolvers": "2.9.0",
|
|
55
55
|
"@next/third-parties": "16.1.6",
|
|
56
56
|
"@react-google-maps/api": "2.17.1",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"yup": "0.32.11"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@akinon/eslint-plugin-projectzero": "2.0.0-beta.
|
|
76
|
+
"@akinon/eslint-plugin-projectzero": "2.0.0-beta.22",
|
|
77
77
|
"@semantic-release/changelog": "6.0.2",
|
|
78
78
|
"@semantic-release/exec": "6.0.3",
|
|
79
79
|
"@semantic-release/git": "10.0.1",
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* next-auth v4 -> v5 migration codemod for Project Zero brands.
|
|
3
|
+
*
|
|
4
|
+
* Handles two brand patterns:
|
|
5
|
+
* Tip A (re-export only): brand's [...nextauth].ts is a 3-line
|
|
6
|
+
* `import Auth from '@akinon/next/api/auth'; export default Auth;`
|
|
7
|
+
* → fully automated; brand's auth.ts + route.ts are generated.
|
|
8
|
+
*
|
|
9
|
+
* Tip B (custom nextAuthOptions): brand has its own config using
|
|
10
|
+
* Pages API req/res. Skeleton is generated and original logic is
|
|
11
|
+
* transferred with TODO markers. Manual conversion of req/res to
|
|
12
|
+
* cookies()/headers() is required afterwards.
|
|
13
|
+
*
|
|
14
|
+
* In both cases, client usage `getServerSession` is rewritten to `auth`
|
|
15
|
+
* via the sibling `transform.js` jscodeshift script.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* cd <brand-root>
|
|
19
|
+
* node <pz-next>/packages/projectzero/codemods/migrate-auth-v5/index.js [--dry-run]
|
|
20
|
+
*
|
|
21
|
+
* Or via the projectzero CLI:
|
|
22
|
+
* npx @akinon/projectzero codemod --codemod=migrate-auth-v5
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const jscodeshift = require('jscodeshift/src/Runner');
|
|
28
|
+
|
|
29
|
+
const TIP_A_SIZE_LIMIT = 256;
|
|
30
|
+
|
|
31
|
+
const TIP_A_AUTH_CONTENT = `import { createAuth } from '@akinon/next/api/auth';
|
|
32
|
+
|
|
33
|
+
export const { handlers, auth, signIn, signOut } = createAuth();
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const ROUTE_CONTENT = `import { handlers } from 'auth';
|
|
37
|
+
|
|
38
|
+
export const { GET, POST } = handlers;
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const TIP_B_HEADER = `/**
|
|
42
|
+
* NOTE: This file was generated by \`migrate-auth-v5\`.
|
|
43
|
+
* It replaces \`src/pages/api/auth/[...nextauth].ts\` which used
|
|
44
|
+
* the Pages API \`req\`/\`res\` objects that next-auth v5 (App Router)
|
|
45
|
+
* no longer provides.
|
|
46
|
+
*
|
|
47
|
+
* MANUAL MIGRATION STEPS (required before first run):
|
|
48
|
+
*
|
|
49
|
+
* 1. \`req.cookies['X']\`
|
|
50
|
+
* -> \`(await cookies()).get('X')?.value\`
|
|
51
|
+
*
|
|
52
|
+
* 2. \`req.headers['X']\` or \`req.headers.X\`
|
|
53
|
+
* -> \`(await headers()).get('X') ?? ''\`
|
|
54
|
+
*
|
|
55
|
+
* 3. \`res.setHeader('Set-Cookie', ['name=value; Path=/; HttpOnly; ...'])\`
|
|
56
|
+
* -> \`(await cookies()).set('name', value, { path: '/', httpOnly: true, secure: true, maxAge })\`
|
|
57
|
+
*
|
|
58
|
+
* 4. \`throw new Error(JSON.stringify(errors))\`
|
|
59
|
+
* -> \`class PzCredentialsError extends CredentialsSignin { constructor(errs) { super(); this.code = JSON.stringify(errs); } }\`
|
|
60
|
+
* -> \`throw new PzCredentialsError(errors)\`
|
|
61
|
+
* (Import \`CredentialsSignin\` from \`'next-auth'\`.)
|
|
62
|
+
*
|
|
63
|
+
* 5. Each \`authorize\` callback is now \`async\` and no longer receives
|
|
64
|
+
* the outer \`req\`/\`res\`. Wrap cookie/header reads in \`await\`.
|
|
65
|
+
*
|
|
66
|
+
* Reference implementation (copy the shape as needed):
|
|
67
|
+
* packages/akinon-next/api/auth.ts on the pz-next beta branch.
|
|
68
|
+
*
|
|
69
|
+
* Search for \`TODO:v5\` markers below for spots that definitely need work.
|
|
70
|
+
*/`;
|
|
71
|
+
|
|
72
|
+
function log(msg) {
|
|
73
|
+
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
74
|
+
console.log(`[${ts}] ${msg}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function detectAuthType(cwd) {
|
|
78
|
+
const oldPath = path.join(cwd, 'src', 'pages', 'api', 'auth', '[...nextauth].ts');
|
|
79
|
+
if (!fs.existsSync(oldPath)) {
|
|
80
|
+
return { type: null, oldPath };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const stats = fs.statSync(oldPath);
|
|
84
|
+
const content = fs.readFileSync(oldPath, 'utf-8');
|
|
85
|
+
|
|
86
|
+
const isReExport =
|
|
87
|
+
stats.size < TIP_A_SIZE_LIMIT &&
|
|
88
|
+
/from\s+['"]@akinon\/next\/api\/auth['"]/.test(content) &&
|
|
89
|
+
/export\s+default\s+Auth/.test(content);
|
|
90
|
+
|
|
91
|
+
return { type: isReExport ? 'A' : 'B', oldPath, content };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ensureParentDir(filePath, { dryRun }) {
|
|
95
|
+
const dir = path.dirname(filePath);
|
|
96
|
+
if (!fs.existsSync(dir)) {
|
|
97
|
+
if (dryRun) {
|
|
98
|
+
log(`[DRY] mkdir -p ${dir}`);
|
|
99
|
+
} else {
|
|
100
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function writeFileSafe(filePath, content, { dryRun }) {
|
|
106
|
+
ensureParentDir(filePath, { dryRun });
|
|
107
|
+
if (dryRun) {
|
|
108
|
+
log(`[DRY] CREATE ${filePath} (${content.length} bytes)`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (fs.existsSync(filePath)) {
|
|
112
|
+
log(` WARN: ${filePath} already exists; overwriting`);
|
|
113
|
+
}
|
|
114
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
115
|
+
log(` CREATE ${filePath}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function deleteFileSafe(filePath, { dryRun }) {
|
|
119
|
+
if (!fs.existsSync(filePath)) return;
|
|
120
|
+
if (dryRun) {
|
|
121
|
+
log(`[DRY] DELETE ${filePath}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
fs.unlinkSync(filePath);
|
|
125
|
+
log(` DELETE ${filePath}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function cleanupEmptyAuthPagesDir(cwd, { dryRun }) {
|
|
129
|
+
const authDir = path.join(cwd, 'src', 'pages', 'api', 'auth');
|
|
130
|
+
if (!fs.existsSync(authDir)) return;
|
|
131
|
+
const remaining = fs.readdirSync(authDir);
|
|
132
|
+
if (remaining.length > 0) return;
|
|
133
|
+
if (dryRun) {
|
|
134
|
+
log(`[DRY] rmdir ${authDir}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
fs.rmdirSync(authDir);
|
|
138
|
+
log(` RMDIR ${authDir}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function migrateTipA(cwd, { dryRun, oldPath }) {
|
|
142
|
+
const authPath = path.join(cwd, 'src', 'auth.ts');
|
|
143
|
+
const routePath = path.join(
|
|
144
|
+
cwd,
|
|
145
|
+
'src',
|
|
146
|
+
'app',
|
|
147
|
+
'api',
|
|
148
|
+
'auth',
|
|
149
|
+
'[...nextauth]',
|
|
150
|
+
'route.ts'
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
log(`Tip A migration`);
|
|
154
|
+
deleteFileSafe(oldPath, { dryRun });
|
|
155
|
+
writeFileSafe(authPath, TIP_A_AUTH_CONTENT, { dryRun });
|
|
156
|
+
writeFileSafe(routePath, ROUTE_CONTENT, { dryRun });
|
|
157
|
+
cleanupEmptyAuthPagesDir(cwd, { dryRun });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function transformTipBSource(original) {
|
|
161
|
+
let src = original;
|
|
162
|
+
|
|
163
|
+
src = src.replace(
|
|
164
|
+
/import\s*\{\s*NextApiRequest\s*,\s*NextApiResponse\s*\}\s*from\s*['"]next['"];?\s*\n/,
|
|
165
|
+
''
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
src = src.replace(
|
|
169
|
+
/import\s+NextAuth(?:\s*,\s*\{([^}]*)\})?\s+from\s+['"]next-auth['"];?\s*\n/,
|
|
170
|
+
(_match, typeImports) => {
|
|
171
|
+
const lines = [
|
|
172
|
+
`import { createAuth } from '@akinon/next/api/auth';`,
|
|
173
|
+
`import { cookies, headers } from 'next/headers';`
|
|
174
|
+
];
|
|
175
|
+
if (typeImports && typeImports.trim()) {
|
|
176
|
+
const cleaned = typeImports
|
|
177
|
+
.split(',')
|
|
178
|
+
.map((s) => s.trim())
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.join(', ');
|
|
181
|
+
if (cleaned) {
|
|
182
|
+
lines.push(`import type { ${cleaned} } from 'next-auth';`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return lines.join('\n') + '\n';
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
src = src.replace(
|
|
190
|
+
/const\s+nextAuthOptions\s*=\s*\(\s*req\s*:\s*NextApiRequest\s*,\s*res\s*:\s*NextApiResponse\s*\)\s*=>\s*\{/,
|
|
191
|
+
`const getCustomAuthConfig = () => {\n // TODO:v5 See migration notes at top of file.`
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
src = src.replace(
|
|
195
|
+
/const\s+nextAuthOptions\s*=\s*\(\s*req\s*,\s*res\s*\)\s*=>\s*\{/,
|
|
196
|
+
`const getCustomAuthConfig = () => {\n // TODO:v5 See migration notes at top of file.`
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
src = src.replace(
|
|
200
|
+
/const\s+Auth\s*=\s*\(\s*req[^)]*\)\s*=>\s*\{\s*return\s+NextAuth\(\s*req\s*,\s*res\s*,\s*nextAuthOptions\(\s*req\s*,\s*res\s*\)\s*\);\s*\};?\s*\n/,
|
|
201
|
+
''
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
src = src.replace(
|
|
205
|
+
/const\s+Auth\s*=\s*\(\s*req[^)]*\)\s*=>\s*NextAuth\([^;]*\);\s*\n/,
|
|
206
|
+
''
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
src = src.replace(
|
|
210
|
+
/export\s+default\s+Auth\s*;?\s*$/m,
|
|
211
|
+
`export const { handlers, auth, signIn, signOut } = createAuth(getCustomAuthConfig);\n`
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
src = src.replace(
|
|
215
|
+
/\b(req\.cookies(?:\?\.|\.)[a-zA-Z_$][\w$]*|req\.cookies\[[^\]]+\])/g,
|
|
216
|
+
'/* TODO:v5 cookies() */ $1'
|
|
217
|
+
);
|
|
218
|
+
src = src.replace(
|
|
219
|
+
/\b(req\.headers(?:\?\.|\.)[a-zA-Z_$][\w$]*|req\.headers\[[^\]]+\])/g,
|
|
220
|
+
'/* TODO:v5 headers() */ $1'
|
|
221
|
+
);
|
|
222
|
+
src = src.replace(
|
|
223
|
+
/\b(req\.body(?:\?\.|\.)?[a-zA-Z_$]?[\w$]*)/g,
|
|
224
|
+
'/* TODO:v5 read body via credentials */ $1'
|
|
225
|
+
);
|
|
226
|
+
src = src.replace(
|
|
227
|
+
/\b(res\.[a-zA-Z_$][\w$]*\()/g,
|
|
228
|
+
'/* TODO:v5 cookies().set */ $1'
|
|
229
|
+
);
|
|
230
|
+
src = src.replace(
|
|
231
|
+
/\bthrow\s+new\s+Error\(JSON\.stringify\(errors\)\)/g,
|
|
232
|
+
'/* TODO:v5 CredentialsSignin */ throw new Error(JSON.stringify(errors))'
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return `${TIP_B_HEADER}\n\n${src}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function migrateTipB(cwd, { dryRun, oldPath, content }) {
|
|
239
|
+
const authPath = path.join(cwd, 'src', 'auth.ts');
|
|
240
|
+
const routePath = path.join(
|
|
241
|
+
cwd,
|
|
242
|
+
'src',
|
|
243
|
+
'app',
|
|
244
|
+
'api',
|
|
245
|
+
'auth',
|
|
246
|
+
'[...nextauth]',
|
|
247
|
+
'route.ts'
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
log(`Tip B migration (${content.length} bytes to transfer)`);
|
|
251
|
+
|
|
252
|
+
const newAuthContent = transformTipBSource(content);
|
|
253
|
+
|
|
254
|
+
writeFileSafe(authPath, newAuthContent, { dryRun });
|
|
255
|
+
writeFileSafe(routePath, ROUTE_CONTENT, { dryRun });
|
|
256
|
+
deleteFileSafe(oldPath, { dryRun });
|
|
257
|
+
cleanupEmptyAuthPagesDir(cwd, { dryRun });
|
|
258
|
+
|
|
259
|
+
log(` Manual work left: req/res -> cookies()/headers() conversions inside ${authPath}`);
|
|
260
|
+
log(` Grep for 'TODO:v5' to find spots.`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function runClientTransform(cwd, { dryRun }) {
|
|
264
|
+
const transformPath = path.resolve(__dirname, 'transform.js');
|
|
265
|
+
const targets = ['src'].map((d) => path.join(cwd, d)).filter(fs.existsSync);
|
|
266
|
+
if (targets.length === 0) {
|
|
267
|
+
log(`No src/ directory found, skipping client transform`);
|
|
268
|
+
return Promise.resolve();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
log(`Running client transform (getServerSession -> auth)`);
|
|
272
|
+
|
|
273
|
+
return jscodeshift
|
|
274
|
+
.run(transformPath, targets, {
|
|
275
|
+
verbose: 0,
|
|
276
|
+
dry: Boolean(dryRun),
|
|
277
|
+
print: Boolean(dryRun),
|
|
278
|
+
extensions: 'ts,tsx',
|
|
279
|
+
parser: 'tsx',
|
|
280
|
+
ignorePattern: '**/node_modules/**',
|
|
281
|
+
silent: true
|
|
282
|
+
})
|
|
283
|
+
.then(
|
|
284
|
+
(stats) => {
|
|
285
|
+
log(
|
|
286
|
+
` Client transform: ${stats.ok} changed, ${stats.nochange} unchanged, ${stats.error} errored`
|
|
287
|
+
);
|
|
288
|
+
},
|
|
289
|
+
(err) => {
|
|
290
|
+
log(` Client transform error: ${err.message}`);
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const transform = () => {
|
|
296
|
+
const workingDir = path.resolve(process.cwd());
|
|
297
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
298
|
+
|
|
299
|
+
log(
|
|
300
|
+
`migrate-auth-v5 starting in ${workingDir}${dryRun ? ' (DRY RUN)' : ''}`
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const detection = detectAuthType(workingDir);
|
|
304
|
+
|
|
305
|
+
if (!detection.type) {
|
|
306
|
+
log(`No src/pages/api/auth/[...nextauth].ts found, nothing to migrate.`);
|
|
307
|
+
return Promise.resolve();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
log(`Detected pattern: Tip ${detection.type}`);
|
|
311
|
+
|
|
312
|
+
if (detection.type === 'A') {
|
|
313
|
+
migrateTipA(workingDir, { dryRun, oldPath: detection.oldPath });
|
|
314
|
+
} else {
|
|
315
|
+
migrateTipB(workingDir, {
|
|
316
|
+
dryRun,
|
|
317
|
+
oldPath: detection.oldPath,
|
|
318
|
+
content: detection.content
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return runClientTransform(workingDir, { dryRun }).then(() => {
|
|
323
|
+
log(`migrate-auth-v5 done`);
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
module.exports = {
|
|
328
|
+
transform
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
if (require.main === module) {
|
|
332
|
+
const result = transform();
|
|
333
|
+
if (result && typeof result.then === 'function') {
|
|
334
|
+
result.catch((err) => {
|
|
335
|
+
console.error(err);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jscodeshift transform: client-side next-auth v4 -> v5 migration.
|
|
3
|
+
*
|
|
4
|
+
* Converts:
|
|
5
|
+
* import { getServerSession } from 'next-auth/next';
|
|
6
|
+
* const session = await getServerSession();
|
|
7
|
+
*
|
|
8
|
+
* Into:
|
|
9
|
+
* import { auth } from 'auth';
|
|
10
|
+
* const session = await auth();
|
|
11
|
+
*
|
|
12
|
+
* Arguments passed to `getServerSession(authOptions, ...)` are dropped,
|
|
13
|
+
* since `auth()` needs none when using the root `src/auth.ts` config.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const AUTH_IMPORT_SOURCES = ['next-auth/next', 'next-auth'];
|
|
17
|
+
|
|
18
|
+
function transform(fileInfo, api) {
|
|
19
|
+
const j = api.jscodeshift;
|
|
20
|
+
const root = j(fileInfo.source);
|
|
21
|
+
let changed = false;
|
|
22
|
+
|
|
23
|
+
AUTH_IMPORT_SOURCES.forEach((sourceValue) => {
|
|
24
|
+
const imports = root.find(j.ImportDeclaration, {
|
|
25
|
+
source: { value: sourceValue }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
imports.forEach((pathNode) => {
|
|
29
|
+
const specifiers = pathNode.node.specifiers || [];
|
|
30
|
+
const getServerSessionSpec = specifiers.find(
|
|
31
|
+
(s) =>
|
|
32
|
+
s.type === 'ImportSpecifier' &&
|
|
33
|
+
s.imported &&
|
|
34
|
+
s.imported.name === 'getServerSession'
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (!getServerSessionSpec) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const localName = getServerSessionSpec.local
|
|
42
|
+
? getServerSessionSpec.local.name
|
|
43
|
+
: 'getServerSession';
|
|
44
|
+
|
|
45
|
+
const otherSpecs = specifiers.filter((s) => s !== getServerSessionSpec);
|
|
46
|
+
|
|
47
|
+
if (otherSpecs.length === 0) {
|
|
48
|
+
pathNode.node.specifiers = [
|
|
49
|
+
j.importSpecifier(j.identifier('auth'))
|
|
50
|
+
];
|
|
51
|
+
pathNode.node.source = j.literal('auth');
|
|
52
|
+
} else {
|
|
53
|
+
pathNode.node.specifiers = otherSpecs;
|
|
54
|
+
const authImport = j.importDeclaration(
|
|
55
|
+
[j.importSpecifier(j.identifier('auth'))],
|
|
56
|
+
j.literal('auth')
|
|
57
|
+
);
|
|
58
|
+
pathNode.insertAfter(authImport);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
root
|
|
62
|
+
.find(j.CallExpression, { callee: { name: localName } })
|
|
63
|
+
.forEach((callPath) => {
|
|
64
|
+
callPath.node.callee = j.identifier('auth');
|
|
65
|
+
callPath.node.arguments = [];
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
changed = true;
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
AUTH_IMPORT_SOURCES.forEach((sourceValue) => {
|
|
73
|
+
root
|
|
74
|
+
.find(j.ImportDeclaration, { source: { value: sourceValue } })
|
|
75
|
+
.forEach((pathNode) => {
|
|
76
|
+
if (!pathNode.node.specifiers || pathNode.node.specifiers.length === 0) {
|
|
77
|
+
j(pathNode).remove();
|
|
78
|
+
changed = true;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return changed ? root.toSource({ quote: 'single' }) : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = transform;
|