@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/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: "The app imports @vercel/functions, but package.json does not list it. Version 20.82 installed it as a dependency of the SDK. The developer preview makes it an optional peer dependency.",
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
- "Version 20.82 chose 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." +
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, change to vercelUniformEdgeMiddleware (edge mode).",
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
- "Version 20.82 rewrote 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.",
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 vercel entry points of @uniformdev/next-app-router contain @uniformdev/context-edge.",
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: "Version 20.82 rewrote missing routes to /404. The developer preview calls notFound() on the page, so this route no longer renders for missing pages.",
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 ? "Build and start the app, and do the edge mode test of the docs: next build && next start" : "Build and test the app: next build",
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) ? "vercel" : /uniformEdgeMiddleware|handleUniformEdgeRoute/.test(middleware) ? "custom" : undefined,
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
  }