@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
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// Phase 2 steps that the hosts share: the edge middleware, the static params, the preview route
|
|
2
|
+
// and the text edits for them.
|
|
3
|
+
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import ts from "typescript";
|
|
7
|
+
|
|
8
|
+
import { addToBlock } from "../json-text.js";
|
|
9
|
+
import { applyEdits } from "../migrate.js";
|
|
10
|
+
import { listSourceFiles, scriptKind } from "../scan.js";
|
|
11
|
+
|
|
12
|
+
export const SDK = "@uniformdev/next-app-router";
|
|
13
|
+
export const EDGE_HEADER = "x-uniform-edge-origin";
|
|
14
|
+
export const WEBHOOK_REMINDER =
|
|
15
|
+
"In Uniform, make sure that a webhook sends manifest.published, composition.*, entry.*, projectmap.node.* and redirect.* to /api/preview. The migration cannot check this.";
|
|
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"]);
|
|
19
|
+
// The edge middleware factories of the SDK, and their entry points.
|
|
20
|
+
const EDGE_FACTORIES = {
|
|
21
|
+
vercelUniformEdgeMiddleware: `${SDK}/vercel`,
|
|
22
|
+
cfVinextUniformEdgeMiddleware: `${SDK}/cf-vinext`,
|
|
23
|
+
uniformEdgeMiddleware: `${SDK}/edge`,
|
|
24
|
+
handleUniformEdgeRoute: `${SDK}/edge`,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns { changes, contents, manual, reminders } for phase 2 on a host with an edge middleware.
|
|
29
|
+
* `host` is { factory, module, runtime, onRevalidateTags, packages, reminders }:
|
|
30
|
+
* - `factory` and `module`: the edge middleware and its entry point.
|
|
31
|
+
* - `runtime`: true when the middleware must run on the edge runtime, in middleware.ts.
|
|
32
|
+
* - `onRevalidateTags`: { name, module } of the function that the preview route must call, if any.
|
|
33
|
+
* - `packages`: [{ name, range, description }] to add to the dependencies.
|
|
34
|
+
* `base` maps absolute paths to the text after phase 1, so that the two phases make one plan.
|
|
35
|
+
*/
|
|
36
|
+
export function planEdge(root, { base = new Map(), host }) {
|
|
37
|
+
const read = (file) => base.get(file) ?? fs.readFileSync(file, "utf8");
|
|
38
|
+
const changes = [];
|
|
39
|
+
const contents = new Map();
|
|
40
|
+
const manual = [];
|
|
41
|
+
const save = (file, text, fileChanges) => {
|
|
42
|
+
if (text !== read(file)) contents.set(file, text);
|
|
43
|
+
changes.push(...fileChanges);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Step 1: packages.
|
|
47
|
+
const pkgFile = path.join(root, "package.json");
|
|
48
|
+
let pkgText = read(pkgFile);
|
|
49
|
+
const pkg = JSON.parse(pkgText);
|
|
50
|
+
const pkgChanges = [];
|
|
51
|
+
for (const { name, range, description } of host.packages ?? []) {
|
|
52
|
+
if (pkg.dependencies?.[name] || pkg.devDependencies?.[name]) continue;
|
|
53
|
+
pkgText = addToBlock(pkgText, "dependencies", name, range);
|
|
54
|
+
pkgChanges.push({ file: "package.json", rule: "edge-package", description: `Add ${name} ${range}. ${description}` });
|
|
55
|
+
}
|
|
56
|
+
if (pkgChanges.length) save(pkgFile, pkgText, pkgChanges);
|
|
57
|
+
|
|
58
|
+
// Step 2: the middleware.
|
|
59
|
+
const middleware = findMiddlewareFile(root);
|
|
60
|
+
if (!middleware) {
|
|
61
|
+
manual.push(`No middleware.ts was found. Add middleware.ts with ${host.factory}, as in the docs.`);
|
|
62
|
+
} else if (host.runtime && /^proxy\./.test(path.basename(middleware))) {
|
|
63
|
+
manual.push(
|
|
64
|
+
`${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.`
|
|
65
|
+
);
|
|
66
|
+
} else {
|
|
67
|
+
const result = migrateEdgeMiddleware(root, middleware, read(middleware), host);
|
|
68
|
+
save(middleware, result.text, result.changes);
|
|
69
|
+
manual.push(...result.manual);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Steps 3 and 4: static params and the preview route, in all source files.
|
|
73
|
+
const routes = migrateRouteFiles(root, read, { skip: middleware, onRevalidateTags: host.onRevalidateTags });
|
|
74
|
+
for (const { file, text, changes: fileChanges } of routes.files) save(file, text, fileChanges);
|
|
75
|
+
manual.push(...routes.manual);
|
|
76
|
+
|
|
77
|
+
return { changes, contents, manual, reminders: [...(host.reminders ?? []), WEBHOOK_REMINDER] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Runs migrateRouteFile on each source file that imports the SDK. */
|
|
81
|
+
export function migrateRouteFiles(root, read, { skip, onRevalidateTags } = {}) {
|
|
82
|
+
const files = [];
|
|
83
|
+
const manual = [];
|
|
84
|
+
for (const file of listSourceFiles(root)) {
|
|
85
|
+
if (file === skip) continue;
|
|
86
|
+
const text = read(file);
|
|
87
|
+
if (!text.includes(SDK)) continue;
|
|
88
|
+
const result = migrateRouteFile(root, file, text, { onRevalidateTags });
|
|
89
|
+
if (result.changes.length || result.manual.length) {
|
|
90
|
+
files.push({ file, text: result.text, changes: result.changes });
|
|
91
|
+
manual.push(...result.manual);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { files, manual };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function findMiddlewareFile(root) {
|
|
98
|
+
for (const dir of ["", "src"]) {
|
|
99
|
+
for (const name of ["middleware", "proxy"]) {
|
|
100
|
+
for (const ext of [".ts", ".js", ".mts", ".mjs", ".tsx", ".jsx"]) {
|
|
101
|
+
const file = path.join(root, dir, name + ext);
|
|
102
|
+
if (fs.existsSync(file)) return file;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function parse(file, text) {
|
|
110
|
+
return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, scriptKind(file));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function sdkImports(sf) {
|
|
114
|
+
const locals = new Map();
|
|
115
|
+
for (const statement of sf.statements) {
|
|
116
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
117
|
+
const module = statement.moduleSpecifier.text;
|
|
118
|
+
if (!module.startsWith(SDK)) continue;
|
|
119
|
+
const bindings = statement.importClause?.namedBindings;
|
|
120
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
121
|
+
for (const element of bindings.elements) {
|
|
122
|
+
locals.set(element.name.text, { module, imported: (element.propertyName ?? element.name).text, element, statement });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return locals;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The middleware file
|
|
130
|
+
|
|
131
|
+
/** Changes the middleware to `host.factory`. Refer to planEdge for `host`. */
|
|
132
|
+
export function migrateEdgeMiddleware(root, file, text, host) {
|
|
133
|
+
const sf = parse(file, text);
|
|
134
|
+
const rel = path.relative(root, file);
|
|
135
|
+
const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
136
|
+
const edits = [];
|
|
137
|
+
const changes = [];
|
|
138
|
+
const manual = [];
|
|
139
|
+
const record = (node, description) => changes.push({ file: rel, line: node ? lineOf(node) : undefined, rule: "edge-middleware", description });
|
|
140
|
+
const { factory, module } = host;
|
|
141
|
+
|
|
142
|
+
const locals = sdkImports(sf);
|
|
143
|
+
const usesEdge = [...locals.values()].some((info) => info.module === module && info.imported === factory);
|
|
144
|
+
const otherEdge = [...locals.values()].find((info) => EDGE_FACTORIES[info.imported] === info.module && info.imported !== factory);
|
|
145
|
+
const lite = [...locals.entries()].find(([, info]) => info.module === `${SDK}/middleware` && LITE.has(info.imported));
|
|
146
|
+
const handle = [...locals.entries()].find(([, info]) => info.module === `${SDK}/middleware` && info.imported === "handleUniformRoute");
|
|
147
|
+
|
|
148
|
+
if (!usesEdge && otherEdge) {
|
|
149
|
+
manual.push(`${rel}: the middleware uses ${otherEdge.imported}. Replace it with ${factory} from ${module}.`);
|
|
150
|
+
} else if (!usesEdge && lite) {
|
|
151
|
+
const [local, info] = lite;
|
|
152
|
+
// Swap the factory only when each reference is a call: uniformMiddleware({ ... }).
|
|
153
|
+
const references = [];
|
|
154
|
+
let other = false;
|
|
155
|
+
const visit = (node) => {
|
|
156
|
+
if (ts.isIdentifier(node) && node.text === local && node !== info.element.name && !ts.isImportSpecifier(node.parent)) {
|
|
157
|
+
if (ts.isCallExpression(node.parent) && node.parent.expression === node) references.push(node);
|
|
158
|
+
else other = true;
|
|
159
|
+
}
|
|
160
|
+
ts.forEachChild(node, visit);
|
|
161
|
+
};
|
|
162
|
+
visit(sf);
|
|
163
|
+
|
|
164
|
+
if (other || !references.length) {
|
|
165
|
+
manual.push(`${rel}: ${local} is used in a way that the migration cannot change. Replace it with ${factory} from ${module}.`);
|
|
166
|
+
} else {
|
|
167
|
+
for (const reference of references) {
|
|
168
|
+
edits.push({ start: reference.getStart(sf), end: reference.end, text: factory });
|
|
169
|
+
record(reference, `Replace ${local}(...) with ${factory}(...). It accepts the same options.`);
|
|
170
|
+
}
|
|
171
|
+
edits.push(...replaceImport(sf, info, factory, module));
|
|
172
|
+
}
|
|
173
|
+
} else if (!usesEdge && handle) {
|
|
174
|
+
manual.push(
|
|
175
|
+
`${rel}: the middleware calls handleUniformRoute in a custom function. Replace it with ${factory}(options)(request, event), or with handleUniformEdgeRoute. The migration does not change custom middleware.`
|
|
176
|
+
);
|
|
177
|
+
} else if (!usesEdge) {
|
|
178
|
+
manual.push(`${rel}: the middleware does not use uniformMiddleware. Add ${factory}, as in the docs.`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// export const config = { matcher, runtime }
|
|
182
|
+
const config = findExportedConst(sf, "config");
|
|
183
|
+
const runtimeHint = host.runtime ? ', and runtime: "experimental-edge"' : "";
|
|
184
|
+
if (!config) {
|
|
185
|
+
const runtimeLine = host.runtime ? `\n runtime: "experimental-edge",` : "";
|
|
186
|
+
const block = `\nexport const config = {\n matcher: [\n {\n source: "${DEFAULT_SOURCE}",\n missing: ${MISSING_RULE},\n },\n ],${runtimeLine}\n};\n`;
|
|
187
|
+
edits.push({ start: text.length, end: text.length, text: (text.endsWith("\n") ? "" : "\n") + block });
|
|
188
|
+
record(
|
|
189
|
+
undefined,
|
|
190
|
+
`Add the middleware config: the default matcher of the docs with the missing header rule${host.runtime ? ', and runtime "experimental-edge"' : ""}. Before, the middleware ran on all paths.`
|
|
191
|
+
);
|
|
192
|
+
} else if (!ts.isObjectLiteralExpression(config)) {
|
|
193
|
+
manual.push(`${rel}: the middleware config is not an object literal. Add missing: ${MISSING_RULE} to each matcher${runtimeHint}.`);
|
|
194
|
+
} else {
|
|
195
|
+
const matcher = findProperty(config, "matcher");
|
|
196
|
+
if (!matcher) {
|
|
197
|
+
edits.push(insertProperty(sf, config, `matcher: [{ source: "${DEFAULT_SOURCE}", missing: ${MISSING_RULE} }]`));
|
|
198
|
+
record(config, "Add a matcher with the missing header rule.");
|
|
199
|
+
} else if (ts.isPropertyAssignment(matcher)) {
|
|
200
|
+
const value = matcher.initializer;
|
|
201
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) {
|
|
202
|
+
edits.push({ start: value.getStart(sf), end: value.end, text: `[{ source: ${value.getText(sf)}, missing: ${MISSING_RULE} }]` });
|
|
203
|
+
record(matcher, "Change the matcher to an object with the missing header rule.");
|
|
204
|
+
} else if (ts.isArrayLiteralExpression(value)) {
|
|
205
|
+
for (const element of value.elements) {
|
|
206
|
+
if (ts.isStringLiteral(element) || ts.isNoSubstitutionTemplateLiteral(element)) {
|
|
207
|
+
edits.push({ start: element.getStart(sf), end: element.end, text: `{ source: ${element.getText(sf)}, missing: ${MISSING_RULE} }` });
|
|
208
|
+
record(element, "Add the missing header rule to the matcher.");
|
|
209
|
+
} else if (ts.isObjectLiteralExpression(element)) {
|
|
210
|
+
const missing = findProperty(element, "missing");
|
|
211
|
+
if (!missing) {
|
|
212
|
+
edits.push(insertProperty(sf, element, `missing: ${MISSING_RULE}`));
|
|
213
|
+
record(element, "Add the missing header rule to the matcher.");
|
|
214
|
+
} else if (!missing.getText(sf).includes(EDGE_HEADER)) {
|
|
215
|
+
if (ts.isPropertyAssignment(missing) && ts.isArrayLiteralExpression(missing.initializer)) {
|
|
216
|
+
edits.push(insertArrayElement(sf, missing.initializer, `{ type: "header", key: "${EDGE_HEADER}" }`));
|
|
217
|
+
record(missing, "Add the edge origin header to the missing rules of the matcher.");
|
|
218
|
+
} else {
|
|
219
|
+
manual.push(`${rel}:${lineOf(missing)}: add { type: "header", key: "${EDGE_HEADER}" } to the missing rules.`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
manual.push(`${rel}:${lineOf(element)}: add missing: ${MISSING_RULE} to this matcher.`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
manual.push(`${rel}:${lineOf(matcher)}: the matcher is not a literal. Add missing: ${MISSING_RULE} to each matcher.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (host.runtime) {
|
|
232
|
+
const runtime = findProperty(config, "runtime");
|
|
233
|
+
if (!runtime) {
|
|
234
|
+
edits.push(insertProperty(sf, config, 'runtime: "experimental-edge"'));
|
|
235
|
+
record(config, 'Add runtime: "experimental-edge". The edge middleware needs the edge runtime.');
|
|
236
|
+
} else if (!/["'`](experimental-)?edge["'`]/.test(runtime.getText(sf))) {
|
|
237
|
+
manual.push(`${rel}:${lineOf(runtime)}: set runtime to "experimental-edge". The edge middleware needs the edge runtime (vercel/next.js#82344).`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { text: applyEdits(text, mergeInsertions(edits)), changes, manual };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function findExportedConst(sf, name) {
|
|
246
|
+
for (const statement of sf.statements) {
|
|
247
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
248
|
+
if (!statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) continue;
|
|
249
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
250
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.text === name) {
|
|
251
|
+
let value = declaration.initializer;
|
|
252
|
+
while (value && (ts.isSatisfiesExpression?.(value) || ts.isAsExpression(value) || ts.isParenthesizedExpression(value))) value = value.expression;
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function findProperty(object, name) {
|
|
261
|
+
return object.properties.find((p) => p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === name);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Route files: createUniformStaticParams and createPreviewPOSTRouteHandler
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Adds edgeMode: [true, false] to createUniformStaticParams. With `onRevalidateTags`
|
|
268
|
+
* ({ name, module, description }), also makes createPreviewPOSTRouteHandler call that function with
|
|
269
|
+
* the revalidated tags.
|
|
270
|
+
*/
|
|
271
|
+
export function migrateRouteFile(root, file, text, { onRevalidateTags } = {}) {
|
|
272
|
+
const sf = parse(file, text);
|
|
273
|
+
const rel = path.relative(root, file);
|
|
274
|
+
const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
275
|
+
const edits = [];
|
|
276
|
+
const changes = [];
|
|
277
|
+
const manual = [];
|
|
278
|
+
const locals = sdkImports(sf);
|
|
279
|
+
const isCall = (node, imported) =>
|
|
280
|
+
ts.isCallExpression(node) && ts.isIdentifier(node.expression) && locals.get(node.expression.text)?.imported === imported;
|
|
281
|
+
let needsImport = false;
|
|
282
|
+
const expire = onRevalidateTags?.name;
|
|
283
|
+
const hasImport = [...locals.values()].some((info) => info.imported === expire);
|
|
284
|
+
const previewChange = onRevalidateTags?.description ?? `Expire the host cache when content is published: onRevalidateTags: ${expire}.`;
|
|
285
|
+
|
|
286
|
+
const visit = (node) => {
|
|
287
|
+
if (isCall(node, "createUniformStaticParams")) {
|
|
288
|
+
const options = node.arguments[0];
|
|
289
|
+
if (options && ts.isObjectLiteralExpression(options)) {
|
|
290
|
+
const edgeMode = findProperty(options, "edgeMode");
|
|
291
|
+
if (!edgeMode) {
|
|
292
|
+
edits.push(insertProperty(sf, options, "edgeMode: [true, false]"));
|
|
293
|
+
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." });
|
|
294
|
+
} else if (!/\btrue\b/.test(edgeMode.getText(sf))) {
|
|
295
|
+
manual.push(`${rel}:${lineOf(edgeMode)}: add true to the edgeMode option of createUniformStaticParams.`);
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
manual.push(`${rel}:${lineOf(node)}: add edgeMode: [true, false] to the options of createUniformStaticParams.`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (expire && isCall(node, "createPreviewPOSTRouteHandler")) {
|
|
303
|
+
const options = node.arguments[0];
|
|
304
|
+
if (!options) {
|
|
305
|
+
const open = sf.getFullText().indexOf("(", node.expression.end);
|
|
306
|
+
edits.push({ start: open + 1, end: node.arguments.end, text: `{ onRevalidateTags: ${expire} }` });
|
|
307
|
+
changes.push({ file: rel, line: lineOf(node), rule: "edge-preview-route", description: previewChange });
|
|
308
|
+
needsImport = true;
|
|
309
|
+
} else if (ts.isObjectLiteralExpression(options)) {
|
|
310
|
+
const existing = findProperty(options, "onRevalidateTags");
|
|
311
|
+
if (!existing) {
|
|
312
|
+
edits.push(insertProperty(sf, options, `onRevalidateTags: ${expire}`));
|
|
313
|
+
changes.push({ file: rel, line: lineOf(node), rule: "edge-preview-route", description: previewChange });
|
|
314
|
+
needsImport = true;
|
|
315
|
+
} else if (!existing.getText(sf).includes(expire)) {
|
|
316
|
+
manual.push(`${rel}:${lineOf(existing)}: you have your own onRevalidateTags. Also call ${expire}(tags) from ${onRevalidateTags.module} in it.`);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
manual.push(`${rel}:${lineOf(node)}: add onRevalidateTags: ${expire} to the options of createPreviewPOSTRouteHandler.`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
ts.forEachChild(node, visit);
|
|
323
|
+
};
|
|
324
|
+
visit(sf);
|
|
325
|
+
|
|
326
|
+
if (needsImport && !hasImport) edits.push(...addNamedImport(sf, expire, onRevalidateTags.module));
|
|
327
|
+
return { text: applyEdits(text, mergeInsertions(edits)), changes, manual };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Text edits
|
|
331
|
+
|
|
332
|
+
/** Inserts `property` as the last property of `object`, in the style of the object. */
|
|
333
|
+
function insertProperty(sf, object, property) {
|
|
334
|
+
const text = sf.getFullText();
|
|
335
|
+
const props = object.properties;
|
|
336
|
+
if (!props.length) {
|
|
337
|
+
return { start: object.getStart(sf) + 1, end: object.end - 1, text: ` ${property} ` };
|
|
338
|
+
}
|
|
339
|
+
const last = props[props.length - 1];
|
|
340
|
+
let at = last.end;
|
|
341
|
+
const comma = /^\s*,/.exec(text.slice(at));
|
|
342
|
+
const trailingComma = Boolean(comma);
|
|
343
|
+
if (comma) at += comma[0].length;
|
|
344
|
+
const multiline = text.slice(object.getStart(sf), object.end).includes("\n");
|
|
345
|
+
if (!multiline) return { start: at, end: at, text: `${trailingComma ? "" : ","} ${property}${trailingComma ? "," : ""}` };
|
|
346
|
+
const indent = /([ \t]*)$/.exec(text.slice(0, last.getStart(sf)))[1];
|
|
347
|
+
return { start: at, end: at, text: `${trailingComma ? "" : ","}\n${indent}${property}${trailingComma ? "," : ""}` };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function insertArrayElement(sf, array, element) {
|
|
351
|
+
const fake = { properties: array.elements, getStart: (s) => array.getStart(s), end: array.end };
|
|
352
|
+
return insertProperty(sf, fake, element);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Replaces the import of `info` with `name` from `module`. */
|
|
356
|
+
function replaceImport(sf, info, name, module) {
|
|
357
|
+
const statement = info.statement;
|
|
358
|
+
const named = statement.importClause.namedBindings;
|
|
359
|
+
const quote = statement.moduleSpecifier.getText(sf)[0];
|
|
360
|
+
const line = `import { ${name} } from ${quote}${module}${quote};`;
|
|
361
|
+
if (named.elements.length === 1 && !statement.importClause.name) {
|
|
362
|
+
return [{ start: statement.getStart(sf), end: statement.end, text: line }];
|
|
363
|
+
}
|
|
364
|
+
// Remove the specifier, and add the new import after the statement.
|
|
365
|
+
const elements = [...named.elements];
|
|
366
|
+
const i = elements.indexOf(info.element);
|
|
367
|
+
const removal =
|
|
368
|
+
i < elements.length - 1
|
|
369
|
+
? { start: info.element.getStart(sf), end: elements[i + 1].getStart(sf), text: "" }
|
|
370
|
+
: { start: elements[i - 1].end, end: info.element.end, text: "" };
|
|
371
|
+
return [removal, { start: statement.end, end: statement.end, text: `\n${line}` }];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Adds `import { name } from module`, or adds `name` to an existing import from the module. */
|
|
375
|
+
function addNamedImport(sf, name, module) {
|
|
376
|
+
const imports = sf.statements.filter(ts.isImportDeclaration);
|
|
377
|
+
const existing = imports.find((s) => ts.isStringLiteral(s.moduleSpecifier) && s.moduleSpecifier.text === module && s.importClause?.namedBindings && ts.isNamedImports(s.importClause.namedBindings));
|
|
378
|
+
if (existing) {
|
|
379
|
+
const elements = existing.importClause.namedBindings.elements;
|
|
380
|
+
const last = elements[elements.length - 1];
|
|
381
|
+
return [{ start: last.end, end: last.end, text: `, ${name}` }];
|
|
382
|
+
}
|
|
383
|
+
const quote = imports[0]?.moduleSpecifier.getText(sf)[0] ?? '"';
|
|
384
|
+
const line = `import { ${name} } from ${quote}${module}${quote};`;
|
|
385
|
+
const after = imports[imports.length - 1];
|
|
386
|
+
if (after) return [{ start: after.end, end: after.end, text: `\n${line}` }];
|
|
387
|
+
return [{ start: 0, end: 0, text: `${line}\n` }];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Joins insertions at the same position, so that applyEdits accepts them. */
|
|
391
|
+
function mergeInsertions(edits) {
|
|
392
|
+
const out = [];
|
|
393
|
+
for (const edit of [...edits].sort((a, b) => a.start - b.start || a.end - b.end)) {
|
|
394
|
+
const previous = out[out.length - 1];
|
|
395
|
+
if (previous && previous.start === edit.start && previous.end === previous.start && edit.end === edit.start) {
|
|
396
|
+
previous.text += edit.text;
|
|
397
|
+
} else out.push({ ...edit });
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
package/src/edge/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Phase 2: edge mode for each host. Add a provider here when its migration is ready.
|
|
2
2
|
|
|
3
|
+
import { planAkamaiEdge } from "./akamai.js";
|
|
4
|
+
import { planCloudflareEdge } from "./cloudflare.js";
|
|
3
5
|
import { planVercelEdge } from "./vercel.js";
|
|
4
6
|
|
|
5
7
|
const DOCS = "https://docs.uniform.app/docs/sdk/nextjs-app-router/edge-mode";
|
|
@@ -14,9 +16,10 @@ export const PROVIDERS = {
|
|
|
14
16
|
},
|
|
15
17
|
cloudflare: {
|
|
16
18
|
name: "Cloudflare",
|
|
17
|
-
available:
|
|
18
|
-
summary: "
|
|
19
|
+
available: true,
|
|
20
|
+
summary: "cfVinextUniformEdgeMiddleware, for vinext on Cloudflare Workers.",
|
|
19
21
|
docs: `${DOCS}#set-up-edge-mode-on-other-hosts`,
|
|
22
|
+
plan: planCloudflareEdge,
|
|
20
23
|
},
|
|
21
24
|
netlify: {
|
|
22
25
|
name: "Netlify",
|
|
@@ -26,9 +29,10 @@ export const PROVIDERS = {
|
|
|
26
29
|
},
|
|
27
30
|
akamai: {
|
|
28
31
|
name: "Akamai",
|
|
29
|
-
available:
|
|
30
|
-
summary: "
|
|
32
|
+
available: true,
|
|
33
|
+
summary: "akamaiUniformEdgeWorker, an EdgeWorker in front of the app.",
|
|
31
34
|
docs: `${DOCS}#set-up-edge-mode-on-other-hosts`,
|
|
35
|
+
plan: planAkamaiEdge,
|
|
32
36
|
},
|
|
33
37
|
};
|
|
34
38
|
|