@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/edge/vercel.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
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
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
257
|
-
function hasReference(
|
|
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 !==
|
|
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(
|
|
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.
|
|
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
|
|
2
|
-
// developer preview (20.81.1-alpha.
|
|
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)",
|