@uniformdev/next-app-router-upgrade 0.0.0-stage → 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 +289 -2
- package/bin/cli.js +167 -0
- package/package.json +35 -4
- package/src/apply.js +124 -0
- 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 +41 -0
- package/src/edge/vercel.js +38 -0
- package/src/interactive.js +111 -0
- package/src/json-text.js +95 -0
- package/src/migrate.js +386 -0
- package/src/registry.js +25 -0
- package/src/report.js +44 -0
- package/src/rules.js +176 -0
- package/src/scan.js +523 -0
- package/src/session.js +206 -0
package/src/session.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// The migration flow, shared by `migrate` and the interactive mode: plan, show, check, write.
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { gitState, unifiedDiff, validate, writeAll } from "./apply.js";
|
|
7
|
+
import { PROVIDERS } from "./edge/index.js";
|
|
8
|
+
import { planMigration, uniformDependencies } from "./migrate.js";
|
|
9
|
+
import { publishedAt } from "./registry.js";
|
|
10
|
+
import { TARGET_VERSION } from "./report.js";
|
|
11
|
+
import { scanProject } from "./scan.js";
|
|
12
|
+
|
|
13
|
+
export const NAME = "uniform-next-app-router-upgrade";
|
|
14
|
+
|
|
15
|
+
export function colors(enabled) {
|
|
16
|
+
return (code, s) => (enabled ? `\u001b[${code}m${s}\u001b[0m` : s);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Phase 1, and phase 2 when `edge` names a provider, as one plan. */
|
|
20
|
+
export async function buildPlan(root, { edge, uniformData } = {}) {
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
22
|
+
const published = await publishedAt(uniformDependencies(pkg), TARGET_VERSION);
|
|
23
|
+
const phase1 = planMigration(root, { published });
|
|
24
|
+
let changes = phase1.changes.map((change) => ({ ...change, phase: 1 }));
|
|
25
|
+
let contents = phase1.contents;
|
|
26
|
+
let steps = [...phase1.manual];
|
|
27
|
+
let reminders = [];
|
|
28
|
+
|
|
29
|
+
if (edge) {
|
|
30
|
+
const phase2 = PROVIDERS[edge].plan(root, { base: phase1.contents });
|
|
31
|
+
changes = [...changes, ...phase2.changes.map((change) => ({ ...change, phase: 2 }))];
|
|
32
|
+
contents = new Map([...phase1.contents, ...phase2.contents]);
|
|
33
|
+
steps = [...steps, ...phase2.manual];
|
|
34
|
+
reminders = phase2.reminders ?? [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const after = scanProject(root, { uniformDataDir: uniformData, contents });
|
|
38
|
+
return {
|
|
39
|
+
changes,
|
|
40
|
+
contents,
|
|
41
|
+
steps,
|
|
42
|
+
reminders,
|
|
43
|
+
manual: after.findings.filter((f) => f.severity === "error"),
|
|
44
|
+
reviews: after.findings.filter((f) => f.severity === "warning"),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Runs the migration and returns an exit code: 0 no code changes left, 1 manual changes left,
|
|
50
|
+
* 2 nothing was written.
|
|
51
|
+
* `ask(question, defaultYes)` returns a promise of true or false. Without it, the flow uses the
|
|
52
|
+
* options only.
|
|
53
|
+
*/
|
|
54
|
+
export async function executeMigration({ root, edge, dryRun, yes, allowDirty, color, uniformData, ask, interactive = false, diff = true }) {
|
|
55
|
+
const c = colors(color);
|
|
56
|
+
const plan = await buildPlan(root, { edge, uniformData });
|
|
57
|
+
const { changes, contents, steps, reminders, manual, reviews } = plan;
|
|
58
|
+
|
|
59
|
+
printChanges(root, changes, c);
|
|
60
|
+
if (changes.length && (diff === true || (diff === "ask" && ask && (await ask("Show the diff?", true))))) {
|
|
61
|
+
for (const [file, text] of contents) {
|
|
62
|
+
console.log(unifiedDiff(path.relative(root, file), fs.readFileSync(file, "utf8"), text, { color }));
|
|
63
|
+
console.log("");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
printManual(manual, reviews, steps, c);
|
|
67
|
+
if (reminders.length) {
|
|
68
|
+
console.log(c("36;1", "Reminders:"));
|
|
69
|
+
for (const reminder of reminders) console.log(` ${c("36", "•")} ${reminder}`);
|
|
70
|
+
console.log("");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const exitCode = manual.length || steps.length ? 1 : 0;
|
|
74
|
+
if (!changes.length || !contents.size) return exitCode;
|
|
75
|
+
|
|
76
|
+
const problems = validate(contents);
|
|
77
|
+
if (problems.length) {
|
|
78
|
+
console.error(c("31;1", "Nothing was written. A change makes a file that does not parse:"));
|
|
79
|
+
for (const problem of problems) console.error(` ${path.relative(root, problem.file)}: ${problem.message}`);
|
|
80
|
+
console.error("Please report this problem to Uniform, with the file.");
|
|
81
|
+
return 2;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (dryRun) {
|
|
85
|
+
console.log(c("1", "Dry run: no files were changed."));
|
|
86
|
+
return exitCode;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const git = gitState(root, [...contents.keys()]);
|
|
90
|
+
if (!allowDirty && (git.notGit || git.dirty.length)) {
|
|
91
|
+
if (git.notGit) {
|
|
92
|
+
console.error(c("31;1", "The app is not in a git repository, so you cannot undo the changes with git."));
|
|
93
|
+
} else {
|
|
94
|
+
console.error(c("31;1", "These files contain changes that are not committed:"));
|
|
95
|
+
for (const file of git.dirty) console.error(` ${file}`);
|
|
96
|
+
}
|
|
97
|
+
// Only the interactive mode asks. With options, --allow-dirty is the only way past this check.
|
|
98
|
+
const proceed = interactive && ask ? await ask("Write the changes anyway?", false) : false;
|
|
99
|
+
if (!proceed) {
|
|
100
|
+
console.error(interactive ? "Nothing was written." : "Nothing was written. Commit or stash the changes first, or use --allow-dirty.");
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!yes) {
|
|
106
|
+
let approved = false;
|
|
107
|
+
if (ask) approved = await ask(`Write ${changes.length} change(s) to ${contents.size} file(s)?`, false);
|
|
108
|
+
else {
|
|
109
|
+
console.error("Nothing was written. To write the changes in a shell without a terminal, use --yes.");
|
|
110
|
+
return 2;
|
|
111
|
+
}
|
|
112
|
+
if (!approved) {
|
|
113
|
+
console.log("Nothing was written.");
|
|
114
|
+
return 2;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
writeAll(contents);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(c("31;1", `Nothing was written: ${error.message}`));
|
|
122
|
+
return 2;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(c("32;1", `Wrote ${changes.length} change(s) to ${contents.size} file(s).`));
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log("Next steps:");
|
|
128
|
+
const next = [
|
|
129
|
+
"Install the new versions: npm install (or pnpm install, or yarn).",
|
|
130
|
+
...(manual.length + steps.length ? [`Do the ${manual.length + steps.length} manual change(s) above.`] : []),
|
|
131
|
+
`Run the check again: ${NAME} check`,
|
|
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",
|
|
137
|
+
"Read the changes: git diff",
|
|
138
|
+
];
|
|
139
|
+
next.forEach((step, i) => console.log(` ${i + 1}. ${step}`));
|
|
140
|
+
if (edge) console.log(c("2", ` Docs: ${PROVIDERS[edge].docs}`));
|
|
141
|
+
console.log("");
|
|
142
|
+
return exitCode;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function printChanges(root, changes, c) {
|
|
146
|
+
if (!changes.length) {
|
|
147
|
+
console.log("No automatic changes are necessary.");
|
|
148
|
+
console.log("");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
for (const phase of [1, 2]) {
|
|
152
|
+
const items = changes.filter((change) => change.phase === phase);
|
|
153
|
+
if (!items.length) continue;
|
|
154
|
+
const title = phase === 1 ? "Phase 1: upgrade the SDK" : "Phase 2: turn on edge mode";
|
|
155
|
+
console.log(c("1", `${title} (${items.length} automatic change(s)):`));
|
|
156
|
+
for (const change of items) console.log(` ${c("32", "•")} ${change.file}${change.line ? `:${change.line}` : ""} ${change.description}`);
|
|
157
|
+
console.log("");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function printManual(manual, reviews, steps, c) {
|
|
162
|
+
if (manual.length || steps.length) {
|
|
163
|
+
console.log(c("31;1", `Manual changes (${manual.length + steps.length}). The migration does not change this code, because the correct change depends on your app:`));
|
|
164
|
+
for (const step of steps) console.log(` ${c("31", "•")} ${step}`);
|
|
165
|
+
for (const f of manual) {
|
|
166
|
+
console.log(` ${c("31", "•")} ${f.file ? `${f.file}${f.line ? `:${f.line}` : ""} ` : ""}${f.message}`);
|
|
167
|
+
console.log(` Fix: ${f.fix}`);
|
|
168
|
+
}
|
|
169
|
+
console.log("");
|
|
170
|
+
} else {
|
|
171
|
+
console.log(c("32", "No manual code changes are necessary."));
|
|
172
|
+
console.log("");
|
|
173
|
+
}
|
|
174
|
+
if (reviews.length) {
|
|
175
|
+
console.log(c("33;1", `Behavior changes to review (${reviews.length}). For the details, run: ${NAME} check`));
|
|
176
|
+
for (const f of reviews) console.log(` ${c("33", "•")} ${f.title}${f.file ? c("2", ` (${f.file}${f.line ? `:${f.line}` : ""})`) : ""}`);
|
|
177
|
+
console.log("");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A short status of the project, for the interactive mode. */
|
|
182
|
+
export function projectStatus(root, { uniformData } = {}) {
|
|
183
|
+
const scan = scanProject(root, { uniformDataDir: uniformData });
|
|
184
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
185
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
186
|
+
const phase1 = planMigration(root);
|
|
187
|
+
const text = (file) => (fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "");
|
|
188
|
+
const middleware = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js", "proxy.ts", "src/proxy.ts"]
|
|
189
|
+
.map((file) => text(path.join(root, file)))
|
|
190
|
+
.join("\n");
|
|
191
|
+
return {
|
|
192
|
+
scan,
|
|
193
|
+
sdkVersion: deps["@uniformdev/next-app-router"],
|
|
194
|
+
atTarget: deps["@uniformdev/next-app-router"] === TARGET_VERSION,
|
|
195
|
+
phase1Pending: phase1.changes.length,
|
|
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,
|
|
205
|
+
};
|
|
206
|
+
}
|