@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.
@@ -1,360 +1,38 @@
1
1
  // Phase 2 on Vercel: turn on edge mode with vercelUniformEdgeMiddleware.
2
2
  // The steps follow "Set up edge mode on Vercel" in the docs.
3
3
 
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import ts from "typescript";
4
+ import { migrateEdgeMiddleware, migrateRouteFile as migrateRoutes, planEdge, SDK } from "./common.js";
7
5
 
8
- import { addToBlock } from "../json-text.js";
9
- import { applyEdits } from "../migrate.js";
10
- import { listSourceFiles, scriptKind } from "../scan.js";
11
-
12
- const SDK = "@uniformdev/next-app-router";
13
6
  const VERCEL = `${SDK}/vercel`;
14
- const EDGE_HEADER = "x-uniform-edge-origin";
15
7
  const VERCEL_FUNCTIONS_RANGE = "^3.7.2";
16
- const DEFAULT_SOURCE = "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)";
17
- const MISSING_RULE = `[{ type: "header", key: "${EDGE_HEADER}" }]`;
18
- const LITE = new Set(["uniformMiddleware"]);
8
+
9
+ const HOST = {
10
+ factory: "vercelUniformEdgeMiddleware",
11
+ module: VERCEL,
12
+ runtime: true,
13
+ onRevalidateTags: {
14
+ name: "expireVercelRuntimeCacheTags",
15
+ module: VERCEL,
16
+ description: "Expire the Vercel runtime cache when content is published: onRevalidateTags: expireVercelRuntimeCacheTags.",
17
+ },
18
+ packages: [{ name: "@vercel/functions", range: VERCEL_FUNCTIONS_RANGE, description: "vercelUniformEdgeMiddleware needs it." }],
19
+ reminders: [
20
+ "If the deployment uses Vercel Deployment Protection, turn on Protection Bypass for Automation in the project settings. Then the edge middleware can fetch the page that it processes.",
21
+ ],
22
+ };
19
23
 
20
24
  /**
21
25
  * Returns { changes, contents, manual, reminders } for phase 2 on Vercel. Reminders are not code changes.
22
26
  * `base` maps absolute paths to the text after phase 1, so that the two phases make one plan.
23
27
  */
24
- export function planVercelEdge(root, { base = new Map() } = {}) {
25
- const read = (file) => base.get(file) ?? fs.readFileSync(file, "utf8");
26
- const changes = [];
27
- const contents = new Map();
28
- const manual = [];
29
- const save = (file, text, fileChanges) => {
30
- if (text !== read(file)) contents.set(file, text);
31
- changes.push(...fileChanges);
32
- };
33
-
34
- // Step 1: @vercel/functions.
35
- const pkgFile = path.join(root, "package.json");
36
- const pkgText = read(pkgFile);
37
- const pkg = JSON.parse(pkgText);
38
- if (!pkg.dependencies?.["@vercel/functions"] && !pkg.devDependencies?.["@vercel/functions"]) {
39
- save(pkgFile, addToBlock(pkgText, "dependencies", "@vercel/functions", VERCEL_FUNCTIONS_RANGE), [
40
- { file: "package.json", rule: "edge-vercel-functions", description: `Add @vercel/functions ${VERCEL_FUNCTIONS_RANGE}. vercelUniformEdgeMiddleware needs it.` },
41
- ]);
42
- }
43
-
44
- // Step 2: the middleware.
45
- const middleware = findMiddlewareFile(root);
46
- if (!middleware) {
47
- manual.push("No middleware.ts was found. Add middleware.ts with vercelUniformEdgeMiddleware, as in the docs.");
48
- } else if (/^proxy\./.test(path.basename(middleware))) {
49
- manual.push(
50
- `${path.relative(root, middleware)}: edge mode needs middleware.ts with runtime "experimental-edge" (vercel/next.js#82344). Rename the file to middleware.ts, and then run the migration again.`
51
- );
52
- } else {
53
- const result = migrateMiddleware(root, middleware, read(middleware));
54
- save(middleware, result.text, result.changes);
55
- manual.push(...result.manual);
56
- }
57
-
58
- // Steps 3 and 4: static params and the preview route, in all source files.
59
- for (const file of listSourceFiles(root)) {
60
- if (file === middleware) continue;
61
- const text = read(file);
62
- if (!text.includes(SDK)) continue;
63
- const result = migrateRouteFile(root, file, text);
64
- if (result.changes.length || result.manual.length) {
65
- save(file, result.text, result.changes);
66
- manual.push(...result.manual);
67
- }
68
- }
69
-
70
- const reminders = [
71
- "In Uniform, make sure that a webhook sends manifest.published, composition.*, entry.*, projectmap.node.* and redirect.* to /api/preview. The migration cannot check this.",
72
- ];
73
- return { changes, contents, manual, reminders };
74
- }
75
-
76
- function findMiddlewareFile(root) {
77
- for (const dir of ["", "src"]) {
78
- for (const name of ["middleware", "proxy"]) {
79
- for (const ext of [".ts", ".js", ".mts", ".mjs", ".tsx", ".jsx"]) {
80
- const file = path.join(root, dir, name + ext);
81
- if (fs.existsSync(file)) return file;
82
- }
83
- }
84
- }
85
- return undefined;
86
- }
87
-
88
- function parse(file, text) {
89
- return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, scriptKind(file));
28
+ export function planVercelEdge(root, { base } = {}) {
29
+ return planEdge(root, { base, host: HOST });
90
30
  }
91
31
 
92
- function sdkImports(sf) {
93
- const locals = new Map();
94
- for (const statement of sf.statements) {
95
- if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
96
- const module = statement.moduleSpecifier.text;
97
- if (!module.startsWith(SDK)) continue;
98
- const bindings = statement.importClause?.namedBindings;
99
- if (bindings && ts.isNamedImports(bindings)) {
100
- for (const element of bindings.elements) {
101
- locals.set(element.name.text, { module, imported: (element.propertyName ?? element.name).text, element, statement });
102
- }
103
- }
104
- }
105
- return locals;
106
- }
107
-
108
- // The middleware file
109
-
110
32
  export function migrateMiddleware(root, file, text) {
111
- const sf = parse(file, text);
112
- const rel = path.relative(root, file);
113
- const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
114
- const edits = [];
115
- const changes = [];
116
- const manual = [];
117
- const record = (node, description) => changes.push({ file: rel, line: node ? lineOf(node) : undefined, rule: "edge-middleware", description });
118
-
119
- const locals = sdkImports(sf);
120
- const usesEdge = [...locals.values()].some((info) => info.module === VERCEL && info.imported === "vercelUniformEdgeMiddleware");
121
- const lite = [...locals.entries()].find(([, info]) => info.module === `${SDK}/middleware` && LITE.has(info.imported));
122
- const handle = [...locals.entries()].find(([, info]) => info.module === `${SDK}/middleware` && info.imported === "handleUniformRoute");
123
-
124
- if (!usesEdge && lite) {
125
- const [local, info] = lite;
126
- // Swap the factory only when each reference is a call: uniformMiddleware({ ... }).
127
- const references = [];
128
- let other = false;
129
- const visit = (node) => {
130
- if (ts.isIdentifier(node) && node.text === local && node !== info.element.name && !ts.isImportSpecifier(node.parent)) {
131
- if (ts.isCallExpression(node.parent) && node.parent.expression === node) references.push(node);
132
- else other = true;
133
- }
134
- ts.forEachChild(node, visit);
135
- };
136
- visit(sf);
137
-
138
- if (other || !references.length) {
139
- manual.push(`${rel}: ${local} is used in a way that the migration cannot change. Replace it with vercelUniformEdgeMiddleware from ${VERCEL}.`);
140
- } else {
141
- for (const reference of references) {
142
- edits.push({ start: reference.getStart(sf), end: reference.end, text: "vercelUniformEdgeMiddleware" });
143
- record(reference, `Replace ${local}(...) with vercelUniformEdgeMiddleware(...). It accepts the same options.`);
144
- }
145
- edits.push(...replaceImport(sf, info, "vercelUniformEdgeMiddleware", VERCEL));
146
- }
147
- } else if (!usesEdge && handle) {
148
- manual.push(
149
- `${rel}: the middleware calls handleUniformRoute in a custom function. Replace it with vercelUniformEdgeMiddleware(options)(request, event), or with handleUniformEdgeRoute. The migration does not change custom middleware.`
150
- );
151
- } else if (!usesEdge) {
152
- manual.push(`${rel}: the middleware does not use uniformMiddleware. Add vercelUniformEdgeMiddleware, as in the docs.`);
153
- }
154
-
155
- // export const config = { matcher, runtime }
156
- const config = findExportedConst(sf, "config");
157
- if (!config) {
158
- const block = `\nexport const config = {\n matcher: [\n {\n source: "${DEFAULT_SOURCE}",\n missing: ${MISSING_RULE},\n },\n ],\n runtime: "experimental-edge",\n};\n`;
159
- edits.push({ start: text.length, end: text.length, text: (text.endsWith("\n") ? "" : "\n") + block });
160
- record(undefined, "Add the middleware config: the default matcher of the docs with the missing header rule, and runtime \"experimental-edge\". Before, the middleware ran on all paths.");
161
- } else if (!ts.isObjectLiteralExpression(config)) {
162
- manual.push(`${rel}: the middleware config is not an object literal. Add missing: ${MISSING_RULE} to each matcher, and runtime: "experimental-edge".`);
163
- } else {
164
- const matcher = findProperty(config, "matcher");
165
- if (!matcher) {
166
- edits.push(insertProperty(sf, config, `matcher: [{ source: "${DEFAULT_SOURCE}", missing: ${MISSING_RULE} }]`));
167
- record(config, "Add a matcher with the missing header rule.");
168
- } else if (ts.isPropertyAssignment(matcher)) {
169
- const value = matcher.initializer;
170
- if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) {
171
- edits.push({ start: value.getStart(sf), end: value.end, text: `[{ source: ${value.getText(sf)}, missing: ${MISSING_RULE} }]` });
172
- record(matcher, "Change the matcher to an object with the missing header rule.");
173
- } else if (ts.isArrayLiteralExpression(value)) {
174
- for (const element of value.elements) {
175
- if (ts.isStringLiteral(element) || ts.isNoSubstitutionTemplateLiteral(element)) {
176
- edits.push({ start: element.getStart(sf), end: element.end, text: `{ source: ${element.getText(sf)}, missing: ${MISSING_RULE} }` });
177
- record(element, "Add the missing header rule to the matcher.");
178
- } else if (ts.isObjectLiteralExpression(element)) {
179
- const missing = findProperty(element, "missing");
180
- if (!missing) {
181
- edits.push(insertProperty(sf, element, `missing: ${MISSING_RULE}`));
182
- record(element, "Add the missing header rule to the matcher.");
183
- } else if (!missing.getText(sf).includes(EDGE_HEADER)) {
184
- if (ts.isPropertyAssignment(missing) && ts.isArrayLiteralExpression(missing.initializer)) {
185
- edits.push(insertArrayElement(sf, missing.initializer, `{ type: "header", key: "${EDGE_HEADER}" }`));
186
- record(missing, "Add the edge origin header to the missing rules of the matcher.");
187
- } else {
188
- manual.push(`${rel}:${lineOf(missing)}: add { type: "header", key: "${EDGE_HEADER}" } to the missing rules.`);
189
- }
190
- }
191
- } else {
192
- manual.push(`${rel}:${lineOf(element)}: add missing: ${MISSING_RULE} to this matcher.`);
193
- }
194
- }
195
- } else {
196
- manual.push(`${rel}:${lineOf(matcher)}: the matcher is not a literal. Add missing: ${MISSING_RULE} to each matcher.`);
197
- }
198
- }
199
-
200
- const runtime = findProperty(config, "runtime");
201
- if (!runtime) {
202
- edits.push(insertProperty(sf, config, 'runtime: "experimental-edge"'));
203
- record(config, 'Add runtime: "experimental-edge". The edge middleware needs the edge runtime.');
204
- } else if (!/["'`](experimental-)?edge["'`]/.test(runtime.getText(sf))) {
205
- manual.push(`${rel}:${lineOf(runtime)}: set runtime to "experimental-edge". The edge middleware needs the edge runtime (vercel/next.js#82344).`);
206
- }
207
- }
208
-
209
- return { text: applyEdits(text, mergeInsertions(edits)), changes, manual };
33
+ return migrateEdgeMiddleware(root, file, text, HOST);
210
34
  }
211
35
 
212
- function findExportedConst(sf, name) {
213
- for (const statement of sf.statements) {
214
- if (!ts.isVariableStatement(statement)) continue;
215
- if (!statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) continue;
216
- for (const declaration of statement.declarationList.declarations) {
217
- if (ts.isIdentifier(declaration.name) && declaration.name.text === name) {
218
- let value = declaration.initializer;
219
- while (value && (ts.isSatisfiesExpression?.(value) || ts.isAsExpression(value) || ts.isParenthesizedExpression(value))) value = value.expression;
220
- return value;
221
- }
222
- }
223
- }
224
- return undefined;
225
- }
226
-
227
- function findProperty(object, name) {
228
- return object.properties.find((p) => p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === name);
229
- }
230
-
231
- // Route files: createUniformStaticParams and createPreviewPOSTRouteHandler
232
-
233
36
  export function migrateRouteFile(root, file, text) {
234
- const sf = parse(file, text);
235
- const rel = path.relative(root, file);
236
- const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
237
- const edits = [];
238
- const changes = [];
239
- const manual = [];
240
- const locals = sdkImports(sf);
241
- const isCall = (node, imported) =>
242
- ts.isCallExpression(node) && ts.isIdentifier(node.expression) && locals.get(node.expression.text)?.imported === imported;
243
- let needsExpireImport = false;
244
- const hasExpireImport = [...locals.values()].some((info) => info.imported === "expireVercelRuntimeCacheTags");
245
-
246
- const visit = (node) => {
247
- if (isCall(node, "createUniformStaticParams")) {
248
- const options = node.arguments[0];
249
- if (options && ts.isObjectLiteralExpression(options)) {
250
- const edgeMode = findProperty(options, "edgeMode");
251
- if (!edgeMode) {
252
- edits.push(insertProperty(sf, options, "edgeMode: [true, false]"));
253
- changes.push({ file: rel, line: lineOf(node), rule: "edge-static-params", description: "Add edgeMode: [true, false], so that the build prebuilds both edge mode values." });
254
- } else if (!/\btrue\b/.test(edgeMode.getText(sf))) {
255
- manual.push(`${rel}:${lineOf(edgeMode)}: add true to the edgeMode option of createUniformStaticParams.`);
256
- }
257
- } else {
258
- manual.push(`${rel}:${lineOf(node)}: add edgeMode: [true, false] to the options of createUniformStaticParams.`);
259
- }
260
- }
261
-
262
- if (isCall(node, "createPreviewPOSTRouteHandler")) {
263
- const options = node.arguments[0];
264
- if (!options) {
265
- const open = sf.getFullText().indexOf("(", node.expression.end);
266
- edits.push({ start: open + 1, end: node.arguments.end, text: "{ onRevalidateTags: expireVercelRuntimeCacheTags }" });
267
- changes.push({ file: rel, line: lineOf(node), rule: "edge-preview-route", description: "Expire the Vercel runtime cache when content is published: onRevalidateTags: expireVercelRuntimeCacheTags." });
268
- needsExpireImport = true;
269
- } else if (ts.isObjectLiteralExpression(options)) {
270
- const existing = findProperty(options, "onRevalidateTags");
271
- if (!existing) {
272
- edits.push(insertProperty(sf, options, "onRevalidateTags: expireVercelRuntimeCacheTags"));
273
- changes.push({ file: rel, line: lineOf(node), rule: "edge-preview-route", description: "Expire the Vercel runtime cache when content is published: onRevalidateTags: expireVercelRuntimeCacheTags." });
274
- needsExpireImport = true;
275
- } else if (!existing.getText(sf).includes("expireVercelRuntimeCacheTags")) {
276
- manual.push(`${rel}:${lineOf(existing)}: you have your own onRevalidateTags. Also call expireVercelRuntimeCacheTags(tags) from ${VERCEL} in it.`);
277
- }
278
- } else {
279
- manual.push(`${rel}:${lineOf(node)}: add onRevalidateTags: expireVercelRuntimeCacheTags to the options of createPreviewPOSTRouteHandler.`);
280
- }
281
- }
282
- ts.forEachChild(node, visit);
283
- };
284
- visit(sf);
285
-
286
- if (needsExpireImport && !hasExpireImport) edits.push(...addNamedImport(sf, "expireVercelRuntimeCacheTags", VERCEL));
287
- return { text: applyEdits(text, mergeInsertions(edits)), changes, manual };
288
- }
289
-
290
- // Text edits
291
-
292
- /** Inserts `property` as the last property of `object`, in the style of the object. */
293
- function insertProperty(sf, object, property) {
294
- const text = sf.getFullText();
295
- const props = object.properties;
296
- if (!props.length) {
297
- return { start: object.getStart(sf) + 1, end: object.end - 1, text: ` ${property} ` };
298
- }
299
- const last = props[props.length - 1];
300
- let at = last.end;
301
- const comma = /^\s*,/.exec(text.slice(at));
302
- const trailingComma = Boolean(comma);
303
- if (comma) at += comma[0].length;
304
- const multiline = text.slice(object.getStart(sf), object.end).includes("\n");
305
- if (!multiline) return { start: at, end: at, text: `${trailingComma ? "" : ","} ${property}${trailingComma ? "," : ""}` };
306
- const indent = /([ \t]*)$/.exec(text.slice(0, last.getStart(sf)))[1];
307
- return { start: at, end: at, text: `${trailingComma ? "" : ","}\n${indent}${property}${trailingComma ? "," : ""}` };
308
- }
309
-
310
- function insertArrayElement(sf, array, element) {
311
- const fake = { properties: array.elements, getStart: (s) => array.getStart(s), end: array.end };
312
- return insertProperty(sf, fake, element);
313
- }
314
-
315
- /** Replaces the import of `info` with `name` from `module`. */
316
- function replaceImport(sf, info, name, module) {
317
- const statement = info.statement;
318
- const named = statement.importClause.namedBindings;
319
- const quote = statement.moduleSpecifier.getText(sf)[0];
320
- const line = `import { ${name} } from ${quote}${module}${quote};`;
321
- if (named.elements.length === 1 && !statement.importClause.name) {
322
- return [{ start: statement.getStart(sf), end: statement.end, text: line }];
323
- }
324
- // Remove the specifier, and add the new import after the statement.
325
- const elements = [...named.elements];
326
- const i = elements.indexOf(info.element);
327
- const removal =
328
- i < elements.length - 1
329
- ? { start: info.element.getStart(sf), end: elements[i + 1].getStart(sf), text: "" }
330
- : { start: elements[i - 1].end, end: info.element.end, text: "" };
331
- return [removal, { start: statement.end, end: statement.end, text: `\n${line}` }];
332
- }
333
-
334
- /** Adds `import { name } from module`, or adds `name` to an existing import from the module. */
335
- function addNamedImport(sf, name, module) {
336
- const imports = sf.statements.filter(ts.isImportDeclaration);
337
- const existing = imports.find((s) => ts.isStringLiteral(s.moduleSpecifier) && s.moduleSpecifier.text === module && s.importClause?.namedBindings && ts.isNamedImports(s.importClause.namedBindings));
338
- if (existing) {
339
- const elements = existing.importClause.namedBindings.elements;
340
- const last = elements[elements.length - 1];
341
- return [{ start: last.end, end: last.end, text: `, ${name}` }];
342
- }
343
- const quote = imports[0]?.moduleSpecifier.getText(sf)[0] ?? '"';
344
- const line = `import { ${name} } from ${quote}${module}${quote};`;
345
- const after = imports[imports.length - 1];
346
- if (after) return [{ start: after.end, end: after.end, text: `\n${line}` }];
347
- return [{ start: 0, end: 0, text: `${line}\n` }];
348
- }
349
-
350
- /** Joins insertions at the same position, so that applyEdits accepts them. */
351
- function mergeInsertions(edits) {
352
- const out = [];
353
- for (const edit of [...edits].sort((a, b) => a.start - b.start || a.end - b.end)) {
354
- const previous = out[out.length - 1];
355
- if (previous && previous.start === edit.start && previous.end === previous.start && edit.end === edit.start) {
356
- previous.text += edit.text;
357
- } else out.push({ ...edit });
358
- }
359
- return out;
37
+ return migrateRoutes(root, file, text, { onRevalidateTags: HOST.onRevalidateTags });
360
38
  }
package/src/migrate.js CHANGED
@@ -6,6 +6,7 @@ import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import ts from "typescript";
8
8
 
9
+ import { COMPOSITION_METADATA, findClientContextIssues } from "./client-context.js";
9
10
  import { TARGET_VERSION } from "./report.js";
10
11
  import { addToBlock, setValueInBlock } from "./json-text.js";
11
12
  import { REMOVED_EXPORTS } from "./rules.js";
@@ -14,7 +15,7 @@ import { listSourceFiles, scriptKind } from "./scan.js";
14
15
  const SDK = "@uniformdev/next-app-router";
15
16
  // These packages have their own version line. The registry check also finds such packages.
16
17
  const SEPARATE_VERSION_LINE = new Set(["@uniformdev/toolbar-react", "@uniformdev/context-edge"]);
17
- // 20.82.0 installed this version of @vercel/functions as a dependency of the SDK.
18
+ // The stable SDK (20.82 to 20.85) installs this version of @vercel/functions as a dependency.
18
19
  const VERCEL_FUNCTIONS_RANGE = "^3.7.2";
19
20
  const DEFAULT_DATA_CLIENT = "DefaultDataClient";
20
21
 
@@ -102,7 +103,7 @@ export function migratePackageJson(text, { importsVercelFunctions, packageManage
102
103
  out = addToBlock(out, "dependencies", "@vercel/functions", VERCEL_FUNCTIONS_RANGE);
103
104
  changes.push({
104
105
  rule: "vercel-functions-transitive",
105
- description: `Add @vercel/functions ${VERCEL_FUNCTIONS_RANGE} to dependencies. Version 20.82 installed it for you.`,
106
+ description: `Add @vercel/functions ${VERCEL_FUNCTIONS_RANGE} to dependencies. The stable SDK installs it for you.`,
106
107
  });
107
108
  }
108
109
 
@@ -219,9 +220,8 @@ export function migrateSourceFile(root, file, text) {
219
220
  };
220
221
  visit(sf);
221
222
 
222
- if (!removals.length) return { text, changes: [], importsVercelFunctions };
223
-
224
- const edits = editsForRemovals(sf, removals);
223
+ const edits = [...editsForRemovals(sf, removals), ...editsForClientContext(sf, sdkLocals, record)];
224
+ if (!edits.length) return { text, changes: [], importsVercelFunctions };
225
225
 
226
226
  // Remove the imports of removed exports that the changes leave without a reference.
227
227
  const removedRanges = edits.map((edit) => [edit.start, edit.end]);
@@ -238,6 +238,36 @@ export function migrateSourceFile(root, file, text) {
238
238
  return { text: applyEdits(text, edits), changes, importsVercelFunctions };
239
239
  }
240
240
 
241
+ // The SDK now sends the composition metadata of the page composition to the Context, so the
242
+ // compositionMetadata argument of useInitUniformContext and its prop have no effect.
243
+ function editsForClientContext(sf, sdkLocals, record) {
244
+ const text = sf.getFullText();
245
+ const { hookArguments, props } = findClientContextIssues(sf, sdkLocals);
246
+ const edits = [];
247
+ for (const { call, argument, removable } of hookArguments) {
248
+ if (!removable) continue;
249
+ let end = argument.end;
250
+ const comma = /^\s*,/.exec(text.slice(end));
251
+ if (comma) end += comma[0].length;
252
+ edits.push({ start: call.arguments[0].end, end, text: "" });
253
+ record(argument, "context-composition-metadata", "Remove the compositionMetadata argument of useInitUniformContext. The SDK sends the metadata of the page composition.");
254
+ }
255
+
256
+ const removedRanges = edits.map((edit) => [edit.start, edit.end]);
257
+ const byPattern = new Map();
258
+ for (const { element, pattern, fn, name } of props) {
259
+ if (name !== COMPOSITION_METADATA || !ts.isIdentifier(element.name)) continue;
260
+ if (hasReference(fn, element.name.text, element, removedRanges, sf)) continue;
261
+ if (!byPattern.has(pattern)) byPattern.set(pattern, new Set());
262
+ byPattern.get(pattern).add(element);
263
+ record(element, "context-composition-metadata", "Remove the compositionMetadata prop. ClientContextComponentProps no longer has it.");
264
+ }
265
+ for (const [pattern, elements] of byPattern) {
266
+ edits.push(...editsForListRemoval(sf, pattern.elements, elements, () => ({ start: pattern.getStart(sf), end: pattern.end, text: "{}" })));
267
+ }
268
+ return edits;
269
+ }
270
+
241
271
  function propertyName(name) {
242
272
  if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
243
273
  return undefined;
@@ -253,14 +283,14 @@ function isServerConfigObject(node, isConfigFile) {
253
283
  return false;
254
284
  }
255
285
 
256
- // True when `local` has a reference in the file outside the import and outside the removed ranges.
257
- function hasReference(sf, local, importElement, removedRanges) {
286
+ // True when `local` has a reference in `scope` outside its declaration and outside the removed ranges.
287
+ function hasReference(scope, local, declaration, removedRanges, sf = scope) {
258
288
  let found = false;
259
289
  const inside = (pos) => removedRanges.some(([start, end]) => pos >= start && pos < end);
260
290
  const visit = (node) => {
261
291
  if (found) return;
262
292
  if (ts.isImportDeclaration(node)) return;
263
- if (ts.isIdentifier(node) && node.text === local && node !== importElement.name && !inside(node.getStart(sf))) {
293
+ if (ts.isIdentifier(node) && node.text === local && node !== declaration.name && !inside(node.getStart(sf))) {
264
294
  // Skip the name part of a property access (x.local) and property names ({ local: x }).
265
295
  const parent = node.parent;
266
296
  const isMemberName = (ts.isPropertyAccessExpression(parent) && parent.name === node) || (ts.isPropertyAssignment(parent) && parent.name === node);
@@ -268,7 +298,7 @@ function hasReference(sf, local, importElement, removedRanges) {
268
298
  }
269
299
  ts.forEachChild(node, visit);
270
300
  };
271
- visit(sf);
301
+ visit(scope);
272
302
  return found;
273
303
  }
274
304
 
package/src/report.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const ORDER = { error: 0, warning: 1, info: 2 };
2
2
  const LABEL = { error: "CODE CHANGE NEEDED", warning: "REVIEW", info: "NOTE" };
3
3
 
4
- export const TARGET_VERSION = "20.81.1-alpha.25.sha-f076f9b857";
4
+ export const TARGET_VERSION = "20.81.1-alpha.41.sha-adcf82dedd";
5
5
 
6
6
  export function verdict(findings, { strict = false } = {}) {
7
7
  const errors = findings.filter((f) => f.severity === "error").length;
package/src/rules.js CHANGED
@@ -1,5 +1,5 @@
1
- // Each rule describes one change between @uniformdev/next-app-router 20.82 and the
2
- // developer preview (20.81.1-alpha.25). "error" rules fail to compile or break at
1
+ // Each rule describes one change between the stable @uniformdev/next-app-router (20.82 to 20.85)
2
+ // and the developer preview (20.81.1-alpha.41). "error" rules fail to compile or break at
3
3
  // runtime. "warning" rules change behavior. "info" rules need no change.
4
4
 
5
5
  const DOCS = "https://docs.uniform.app/docs/sdk/nextjs-app-router";
@@ -78,6 +78,21 @@ export const RULES = {
78
78
  title: "Next.js version is lower than 16.0.7",
79
79
  docs: `${DOCS}#prerequisites`,
80
80
  },
81
+ "context-composition-metadata": {
82
+ severity: "error",
83
+ title: "compositionMetadata in a custom client context component",
84
+ docs: `${DOCS}/client-context`,
85
+ },
86
+ "context-storage-options": {
87
+ severity: "error",
88
+ title: "Visitor data option of 20.85 that the developer preview does not have",
89
+ docs: `${DOCS}/client-context`,
90
+ },
91
+ "uniform-context-props": {
92
+ severity: "error",
93
+ title: "UniformContext with result or children (changed after alpha.25)",
94
+ docs: `${DOCS}/client-context`,
95
+ },
81
96
  "vercel-functions-transitive": {
82
97
  severity: "error",
83
98
  title: "@vercel/functions is imported but not a dependency",
@@ -148,6 +163,11 @@ export const RULES = {
148
163
  title: "@uniformdev/context-edge is no longer necessary",
149
164
  docs: `${UPGRADE}#step-1-install-the-packages`,
150
165
  },
166
+ "playground-path": {
167
+ severity: "info",
168
+ title: "The middleware accepts processPlaygroundPath",
169
+ docs: `${DOCS}/preview`,
170
+ },
151
171
  "redirect-status": {
152
172
  severity: "info",
153
173
  title: "Uniform redirects now come from the page (308 or 307)",