@uniformdev/next-app-router-upgrade 0.1.0 → 0.2.0
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 +49 -8
- package/package.json +1 -1
- package/src/client-context.js +89 -0
- package/src/edge/akamai.js +62 -0
- package/src/edge/cloudflare.js +45 -0
- package/src/edge/common.js +400 -0
- package/src/edge/index.js +8 -4
- package/src/edge/vercel.js +20 -342
- package/src/migrate.js +39 -9
- package/src/report.js +1 -1
- package/src/rules.js +22 -2
- package/src/scan.js +86 -6
- package/src/session.js +14 -2
package/src/scan.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import ts from "typescript";
|
|
4
4
|
|
|
5
|
+
import { COMPOSITION_METADATA, findClientContextIssues, STABLE_CONTEXT_OPTIONS } from "./client-context.js";
|
|
5
6
|
import {
|
|
6
7
|
REMOVED_CONFIG_KEYS,
|
|
7
8
|
REMOVED_EXPORTS,
|
|
@@ -16,6 +17,7 @@ const SDK = "@uniformdev/next-app-router";
|
|
|
16
17
|
const RESOLVERS = new Set(["resolveRouteFromCode", "resolvePlaygroundRoute"]);
|
|
17
18
|
const LITE_MIDDLEWARE = new Set(["uniformMiddleware", "handleUniformRoute"]);
|
|
18
19
|
const MIN_NEXT = [16, 0, 7];
|
|
20
|
+
const STABLE = "The stable SDK (20.82 to 20.85)";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Scans the project at `root` and returns { project, findings }.
|
|
@@ -34,6 +36,7 @@ export function scanProject(root, { uniformDataDir, contents } = {}) {
|
|
|
34
36
|
name: pkg?.name,
|
|
35
37
|
sdkVersion: deps[SDK],
|
|
36
38
|
usesV1: Boolean(deps["@uniformdev/canvas-next-rsc"]),
|
|
39
|
+
usesAkamaiWorker: false,
|
|
37
40
|
filesScanned: 0,
|
|
38
41
|
compositionsWithPersonalization: 0,
|
|
39
42
|
};
|
|
@@ -46,12 +49,21 @@ export function scanProject(root, { uniformDataDir, contents } = {}) {
|
|
|
46
49
|
for (const file of files) {
|
|
47
50
|
scanSourceFile(root, file, add, state, read);
|
|
48
51
|
}
|
|
52
|
+
project.usesAkamaiWorker = Boolean(state.usesAkamaiWorker);
|
|
53
|
+
|
|
54
|
+
if (state.processPlaygroundPath && !state.middlewarePassesPlaygroundPath) {
|
|
55
|
+
add("playground-path", {
|
|
56
|
+
...state.processPlaygroundPath,
|
|
57
|
+
message: "createPreviewGETRouteHandler gets processPlaygroundPath. The middleware of the developer preview accepts the same option, so that it finds the playground path for the locale.",
|
|
58
|
+
fix: "Give the same processPlaygroundPath function to the middleware.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
49
61
|
|
|
50
62
|
if (state.importsVercelFunctions.length && !deps["@vercel/functions"]) {
|
|
51
63
|
for (const where of state.importsVercelFunctions) {
|
|
52
64
|
add("vercel-functions-transitive", {
|
|
53
65
|
...where,
|
|
54
|
-
message:
|
|
66
|
+
message: `The app imports @vercel/functions, but package.json does not list it. ${STABLE} installs it as a dependency of the SDK. The developer preview makes it an optional peer dependency.`,
|
|
55
67
|
fix: "Add @vercel/functions to the dependencies in package.json.",
|
|
56
68
|
});
|
|
57
69
|
}
|
|
@@ -65,16 +77,16 @@ export function scanProject(root, { uniformDataDir, contents } = {}) {
|
|
|
65
77
|
add("lite-mode-flicker", {
|
|
66
78
|
...state.liteMiddlewareLocation,
|
|
67
79
|
message:
|
|
68
|
-
|
|
80
|
+
`${STABLE} chooses personalization and test variants in the middleware. With uniformMiddleware or handleUniformRoute, the developer preview renders the default variants on the server, and the browser changes them after hydration.` +
|
|
69
81
|
(count ? ` The uniform-data folder has ${count} composition(s) with a personalization or a test.` : ""),
|
|
70
|
-
fix: "No code change is required. To keep the first paint correct,
|
|
82
|
+
fix: "No code change is required. To keep the first paint correct, turn on edge mode: migrate --edge <host>.",
|
|
71
83
|
});
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
if (project.sdkVersion) {
|
|
75
87
|
add("not-found-html", {
|
|
76
88
|
message:
|
|
77
|
-
|
|
89
|
+
`${STABLE} rewrites missing routes to /404 in the middleware. The developer preview calls notFound() while the page renders. The response is still 404, but Next.js sends an empty HTML shell, and the 404 content (also app/not-found.tsx) shows only after hydration. Tested with Next.js 16.2.10 and 16.4.0.`,
|
|
78
90
|
fix: "No code change is required. Visitors without JavaScript see an empty 404 page.",
|
|
79
91
|
});
|
|
80
92
|
add("redirect-status", {
|
|
@@ -143,7 +155,7 @@ function checkPackageJson(pkg, deps, add) {
|
|
|
143
155
|
if (deps["@uniformdev/context-edge"]) {
|
|
144
156
|
add("context-edge-dependency", {
|
|
145
157
|
...where,
|
|
146
|
-
message: "The edge and
|
|
158
|
+
message: "The edge, vercel, cf-vinext and akamai entry points of @uniformdev/next-app-router contain @uniformdev/context-edge.",
|
|
147
159
|
fix: "You can remove @uniformdev/context-edge from package.json, if the app does not import it.",
|
|
148
160
|
});
|
|
149
161
|
}
|
|
@@ -201,6 +213,7 @@ function scanSourceFile(root, file, add, state, read) {
|
|
|
201
213
|
}
|
|
202
214
|
if (!module.startsWith(SDK)) continue;
|
|
203
215
|
importsSdk = true;
|
|
216
|
+
if (module === `${SDK}/akamai`) state.usesAkamaiWorker = true;
|
|
204
217
|
|
|
205
218
|
if (module === `${SDK}/cache`) {
|
|
206
219
|
add("cached-resolver", {
|
|
@@ -224,6 +237,9 @@ function scanSourceFile(root, file, add, state, read) {
|
|
|
224
237
|
}
|
|
225
238
|
|
|
226
239
|
if (isConfigFile) importsSdk = true;
|
|
240
|
+
if (isMiddlewareFile && text.includes("processPlaygroundPath")) state.middlewarePassesPlaygroundPath = true;
|
|
241
|
+
|
|
242
|
+
scanClientContext(sf, sdkLocals, add, at);
|
|
227
243
|
|
|
228
244
|
const resultVariables = new Set();
|
|
229
245
|
|
|
@@ -243,6 +259,13 @@ function scanSourceFile(root, file, add, state, read) {
|
|
|
243
259
|
if (name && REMOVED_CONFIG_KEYS[name] && (isConfigFile || isServerConfigContext(node))) {
|
|
244
260
|
add("removed-config-key", { ...at(node), message: `The server configuration option "${name}" is removed.`, fix: REMOVED_CONFIG_KEYS[name] });
|
|
245
261
|
}
|
|
262
|
+
if (name && STABLE_CONTEXT_OPTIONS[name] && (isConfigFile || isServerConfigContext(node))) {
|
|
263
|
+
add("context-storage-options", {
|
|
264
|
+
...at(node),
|
|
265
|
+
message: `The server configuration option "context.${name}" came in 20.85.0. The developer preview does not have it.`,
|
|
266
|
+
fix: `Remove it. ${STABLE_CONTEXT_OPTIONS[name]}`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
246
269
|
if (name === "defaultConsent" && isMiddlewareFile && !isBooleanLiteral(node)) {
|
|
247
270
|
add("per-request-consent", {
|
|
248
271
|
...at(node),
|
|
@@ -252,6 +275,30 @@ function scanSourceFile(root, file, add, state, read) {
|
|
|
252
275
|
}
|
|
253
276
|
}
|
|
254
277
|
|
|
278
|
+
// createPreviewGETRouteHandler({ processPlaygroundPath })
|
|
279
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && sdkLocals.get(node.expression.text)?.imported === "createPreviewGETRouteHandler") {
|
|
280
|
+
const options = node.arguments[0];
|
|
281
|
+
const option = options && ts.isObjectLiteralExpression(options) && options.properties.find((p) => p.name && propertyName(p.name) === "processPlaygroundPath");
|
|
282
|
+
if (option) state.processPlaygroundPath ??= at(option);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// <UniformContext result={result}>...</UniformContext>, the API of 20.81.1-alpha.25.
|
|
286
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
287
|
+
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
|
288
|
+
const local = ts.isIdentifier(opening.tagName) ? sdkLocals.get(opening.tagName.text) : undefined;
|
|
289
|
+
if (local?.imported === "UniformContext" && local.module.startsWith(SDK)) {
|
|
290
|
+
const hasResult = opening.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "result");
|
|
291
|
+
const hasChildren = ts.isJsxElement(node) && node.children.some((child) => !(ts.isJsxText(child) && child.containsOnlyTriviaWhiteSpaces));
|
|
292
|
+
if (hasResult || hasChildren) {
|
|
293
|
+
add("uniform-context-props", {
|
|
294
|
+
...at(node),
|
|
295
|
+
message: "UniformContext no longer takes result or children. Render it once after the compositions of the page, or in the root layout. The primary composition sets the composition metadata and the default consent.",
|
|
296
|
+
fix: "Render the compositions, then <UniformContext />. Add pageComposition to the UniformResolvedComposition of the page.",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
255
302
|
// <UniformComposition dataClient={...} />
|
|
256
303
|
if (ts.isJsxAttribute(node) && importsSdk) {
|
|
257
304
|
const name = node.name.getText(sf);
|
|
@@ -331,6 +378,39 @@ function scanSourceFile(root, file, add, state, read) {
|
|
|
331
378
|
visit(sf);
|
|
332
379
|
}
|
|
333
380
|
|
|
381
|
+
function scanClientContext(sf, sdkLocals, add, at) {
|
|
382
|
+
const { hookArguments, props, options } = findClientContextIssues(sf, sdkLocals);
|
|
383
|
+
for (const { argument } of hookArguments) {
|
|
384
|
+
add("context-composition-metadata", {
|
|
385
|
+
...at(argument),
|
|
386
|
+
message: "useInitUniformContext takes only the callback now. The SDK sends the composition metadata of the page composition to the Context.",
|
|
387
|
+
fix: "Remove the second argument of useInitUniformContext.",
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
for (const { element, name } of props) {
|
|
391
|
+
if (name === COMPOSITION_METADATA) {
|
|
392
|
+
add("context-composition-metadata", {
|
|
393
|
+
...at(element),
|
|
394
|
+
message: "ClientContextComponentProps no longer has compositionMetadata.",
|
|
395
|
+
fix: "Remove the prop. Do not give it to useInitUniformContext.",
|
|
396
|
+
});
|
|
397
|
+
} else {
|
|
398
|
+
add("context-storage-options", {
|
|
399
|
+
...at(element),
|
|
400
|
+
message: `ClientContextComponentProps has no "${name}" in the developer preview. It came in 20.85.0.`,
|
|
401
|
+
fix: `Remove the prop. ${STABLE_CONTEXT_OPTIONS[name]}`,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
for (const { property, name } of options) {
|
|
406
|
+
add("context-storage-options", {
|
|
407
|
+
...at(property),
|
|
408
|
+
message: `createClientUniformContext has no "${name}" option in the developer preview. It came in 20.85.0.`,
|
|
409
|
+
fix: `Remove the option. ${STABLE_CONTEXT_OPTIONS[name]}`,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
334
414
|
function checkImportedName(module, imported, node, add, at) {
|
|
335
415
|
const removed = REMOVED_EXPORTS[module]?.[imported];
|
|
336
416
|
if (removed) {
|
|
@@ -436,7 +516,7 @@ function scanNotFoundRoutes(root, files, add) {
|
|
|
436
516
|
for (const file of notFoundRoutes) {
|
|
437
517
|
add("not-found-page", {
|
|
438
518
|
file: path.relative(root, file),
|
|
439
|
-
message:
|
|
519
|
+
message: `${STABLE} rewrites missing routes to /404. The developer preview calls notFound() on the page, so this route no longer renders for missing pages.`,
|
|
440
520
|
fix: "Move the 404 content to app/not-found.tsx.",
|
|
441
521
|
});
|
|
442
522
|
}
|
package/src/session.js
CHANGED
|
@@ -129,7 +129,11 @@ export async function executeMigration({ root, edge, dryRun, yes, allowDirty, co
|
|
|
129
129
|
"Install the new versions: npm install (or pnpm install, or yarn).",
|
|
130
130
|
...(manual.length + steps.length ? [`Do the ${manual.length + steps.length} manual change(s) above.`] : []),
|
|
131
131
|
`Run the check again: ${NAME} check`,
|
|
132
|
-
edge
|
|
132
|
+
edge === "vercel"
|
|
133
|
+
? "Build and start the app, and do the edge mode test of the docs: next build && next start"
|
|
134
|
+
: edge
|
|
135
|
+
? `Build and deploy the app${edge === "akamai" ? " and the EdgeWorker" : ""}, and do the edge mode test of the docs.`
|
|
136
|
+
: "Build and test the app: next build",
|
|
133
137
|
"Read the changes: git diff",
|
|
134
138
|
];
|
|
135
139
|
next.forEach((step, i) => console.log(` ${i + 1}. ${step}`));
|
|
@@ -189,6 +193,14 @@ export function projectStatus(root, { uniformData } = {}) {
|
|
|
189
193
|
sdkVersion: deps["@uniformdev/next-app-router"],
|
|
190
194
|
atTarget: deps["@uniformdev/next-app-router"] === TARGET_VERSION,
|
|
191
195
|
phase1Pending: phase1.changes.length,
|
|
192
|
-
edgeMode: /vercelUniformEdgeMiddleware/.test(middleware)
|
|
196
|
+
edgeMode: /vercelUniformEdgeMiddleware/.test(middleware)
|
|
197
|
+
? "vercel"
|
|
198
|
+
: /cfVinextUniformEdgeMiddleware/.test(middleware)
|
|
199
|
+
? "cloudflare"
|
|
200
|
+
: scan.project.usesAkamaiWorker
|
|
201
|
+
? "akamai"
|
|
202
|
+
: /uniformEdgeMiddleware|handleUniformEdgeRoute/.test(middleware)
|
|
203
|
+
? "custom"
|
|
204
|
+
: undefined,
|
|
193
205
|
};
|
|
194
206
|
}
|