@uniformdev/next-app-router-upgrade 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,249 @@
1
- # Temporary Holding Version
1
+ # @uniformdev/next-app-router-upgrade
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ This CLI helps you move a Next.js App Router app that uses `@uniformdev/next-app-router` to the developer preview of the SDK (`20.81.1-alpha.25.sha-f076f9b857`). The upgrade has two phases:
4
+
5
+ - **Phase 1: upgrade the SDK.** The minimum changes. After phase 1, the app works in lite mode: the browser chooses the personalization and test variants after hydration.
6
+ - **Phase 2: turn on edge mode.** The edge chooses the variants of each visitor, so the first paint is correct. The steps depend on the host of the app.
7
+
8
+ The CLI has these commands:
9
+
10
+ | Command | What it does |
11
+ |---|---|
12
+ | (no command, in a terminal) | Starts the interactive guide. |
13
+ | `interactive` | A guide through the check, phase 1 and phase 2, with questions. |
14
+ | `check` | Scans the app, and tells you if it can move to the developer preview without code changes. Without a terminal, this is the default. |
15
+ | `migrate` | Phase 1: applies the code changes that are safe to do automatically, and lists the manual changes. |
16
+ | `migrate --edge <host>` | Phase 1 and phase 2 in one plan. |
17
+
18
+ ## Interactive guide
19
+
20
+ Run the CLI in the root folder of your Next.js app, without a command:
21
+
22
+ ```bash
23
+ npx @uniformdev/next-app-router-upgrade
24
+ ```
25
+
26
+ The guide shows the status of the app: the SDK version, the result of the check, and the mode (lite mode or edge mode). Then it asks what you want to do:
27
+
28
+ 1. **Check compatibility.** Shows the full report. It changes nothing.
29
+ 2. **Phase 1: upgrade the SDK.** Shows the automatic changes, the diff if you want it, and the manual changes. It writes only after you say yes.
30
+ 3. **Phase 2: turn on edge mode.** Asks for the host of your app. If phase 1 is not done, the plan includes phase 1 too.
31
+
32
+ After a write, the guide can run `npm install` (or `pnpm install`, or `yarn`) for you. Then it goes back to the menu, with the new status.
33
+
34
+ The guide uses the same safety checks as `migrate`. One difference: when a file contains changes that are not committed, the guide asks if you want to continue. `migrate` stops, unless you use `--allow-dirty`.
35
+
36
+ ### Hosts for edge mode
37
+
38
+ | Host | Status |
39
+ |---|---|
40
+ | Vercel | Available: `--edge vercel` |
41
+ | Cloudflare | Coming soon. Follow the docs for other hosts. |
42
+ | Netlify | Coming soon. Follow the docs for other hosts. |
43
+ | Akamai | Coming soon. Follow the docs for other hosts. |
44
+
45
+ ## Usage
46
+
47
+ Run the check in the root folder of your Next.js app:
48
+
49
+ ```bash
50
+ npx @uniformdev/next-app-router-upgrade
51
+ ```
52
+
53
+ Or give the folder:
54
+
55
+ ```bash
56
+ npx @uniformdev/next-app-router-upgrade ./apps/web
57
+ ```
58
+
59
+ | Option | Description |
60
+ |---|---|
61
+ | `--json` | Print the result as JSON, for CI. |
62
+ | `--strict` | Fail on warnings too. |
63
+ | `--uniform-data <dir>` | The folder of serialized Uniform content. The default is `uniform-data`. |
64
+ | `--no-color` | Print without colors. |
65
+
66
+ | Exit code | Meaning |
67
+ |---|---|
68
+ | `0` | Safe to install. Read the warnings: they are behavior changes. |
69
+ | `1` | Code changes needed. |
70
+ | `2` | The check could not run, for example because there is no `package.json`. |
71
+
72
+ ## Migrate
73
+
74
+ Run the migration in the root folder of your Next.js app. First, see the changes without writing files:
75
+
76
+ ```bash
77
+ npx @uniformdev/next-app-router-upgrade migrate --dry-run
78
+ ```
79
+
80
+ The command shows a list of the changes, a diff for each file, the manual changes and the behavior changes to review. Then write the changes:
81
+
82
+ ```bash
83
+ npx @uniformdev/next-app-router-upgrade migrate
84
+ ```
85
+
86
+ The command asks before it writes. After the migration:
87
+
88
+ 1. Install the new versions with `npm install`, `pnpm install` or `yarn`.
89
+ 2. Do the manual changes.
90
+ 3. Run `check` again.
91
+ 4. Build and test the app with `next build`.
92
+ 5. Read the changes with `git diff`.
93
+
94
+ | Option | Description |
95
+ |---|---|
96
+ | `--dry-run` | Show the changes. Do not write files. |
97
+ | `--yes` | Write the changes without a question. Necessary in CI and in a shell without a terminal. |
98
+ | `--allow-dirty` | Write also when the files contain changes that are not committed, or when the app is not in a git repository. |
99
+ | `--no-color` | Print without colors. |
100
+
101
+ | Exit code | Meaning |
102
+ |---|---|
103
+ | `0` | No code changes are left. |
104
+ | `1` | Manual code changes are left. The automatic changes were written, if you did not use `--dry-run`. |
105
+ | `2` | Nothing was written, because of an error or a safety check. |
106
+
107
+ ### Automatic changes
108
+
109
+ The migration changes only code where the result has the same behavior:
110
+
111
+ | Change | When |
112
+ |---|---|
113
+ | Set each `@uniformdev` package to the target version | The npm registry has the package at that version. `@uniformdev/toolbar-react` and `@uniformdev/context-edge` have their own version lines, so they do not change. |
114
+ | Add `@vercel/functions` `^3.7.2` to `dependencies` | The app imports `@vercel/functions`, and `package.json` does not list it. Version 20.82 installed it for you. |
115
+ | Add an override for `@uniformdev/context` | The target is a prerelease, and the app uses `@uniformdev/toolbar-react`. npm gets `overrides`, and yarn gets `resolutions`. For pnpm, the migration gives a manual step. |
116
+ | Remove the `pathPatternsWithVariations` option | Always. The option has no effect in the developer preview. |
117
+ | Remove `dataClient: new DefaultDataClient()` and the `dataClient={new DefaultDataClient()}` prop | Only for the default data client. A custom data client stays, and the migration lists it as a manual change. |
118
+ | Remove `middlewareRuntimeCache` and `experimental.disableSwrMiddlewareCache` | In the server configuration: `uniform.server.config.*`, or an object of the type `UniformServerConfig`. |
119
+ | Remove the import of a removed API | Only when no code uses it after the changes above. |
120
+
121
+ All other findings of `check` are manual changes. Examples are a custom data client, `result.code`, `route.compositionApiResponse` without a type check, old page codes and the Next.js version. The migration shows each one with its file and line.
122
+
123
+ ### Phase 2 on Vercel (`--edge vercel`)
124
+
125
+ ```bash
126
+ npx @uniformdev/next-app-router-upgrade migrate --edge vercel --dry-run
127
+ npx @uniformdev/next-app-router-upgrade migrate --edge vercel
128
+ ```
129
+
130
+ The steps follow "Set up edge mode on Vercel" in the docs:
131
+
132
+ | Change | When |
133
+ |---|---|
134
+ | Add `@vercel/functions` `^3.7.2` | `package.json` does not list it. |
135
+ | Replace `uniformMiddleware(...)` with `vercelUniformEdgeMiddleware(...)`, and import it from `@uniformdev/next-app-router/vercel` | Each reference to `uniformMiddleware` is a call. The edge middleware accepts the same options. |
136
+ | Add `missing: [{ type: "header", key: "x-uniform-edge-origin" }]` to each matcher | A string matcher becomes `{ source, missing }`. An object matcher gets the `missing` rule. |
137
+ | Add `runtime: "experimental-edge"` | The config has no runtime. |
138
+ | Add a config with the default matcher of the docs | `middleware.ts` has no config. Before, the middleware ran on all paths. |
139
+ | Add `edgeMode: [true, false]` to `createUniformStaticParams` | The options do not have `edgeMode`. |
140
+ | Add `onRevalidateTags: expireVercelRuntimeCacheTags` to `createPreviewPOSTRouteHandler`, and its import | The handler does not have `onRevalidateTags`. |
141
+
142
+ These cases are manual changes:
143
+
144
+ - A custom middleware that calls `handleUniformRoute`.
145
+ - A `proxy.ts` file. Edge mode needs `middleware.ts` with the edge runtime (vercel/next.js#82344).
146
+ - A runtime other than `experimental-edge` or `edge`.
147
+ - A matcher or options that are not literals.
148
+ - An own `onRevalidateTags` function. Call `expireVercelRuntimeCacheTags(tags)` from it.
149
+
150
+ The CLI also reminds you to check the Uniform webhook. It cannot see the webhook settings.
151
+
152
+ ### Safety
153
+
154
+ - **Nothing is written without your approval.** The command asks first. In a shell without a terminal, it writes only with `--yes`.
155
+ - **You can undo the changes with git.** The command stops in two cases: a file to change contains changes that are not committed, or the app is not in a git repository. Use `--allow-dirty` to write in these cases.
156
+ - **The changes must parse.** The command parses each changed file again. If a change adds a syntax error, nothing is written.
157
+ - **All files or none.** The command writes each file to a temporary file first. If a write fails, it writes the original files back.
158
+ - **A second run changes nothing.** The changes do not repeat.
159
+ - **The format stays.** The migration changes only the necessary text. Other lines, comments and the format of `package.json` stay the same.
160
+ - **The migration does not install packages.** It does not change the lock file. Run your package manager after the migration.
161
+
162
+ ## What `check` finds
163
+
164
+ The check reads `package.json`, all source files (`.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`) with the TypeScript parser, and the `uniform-data` folder when it exists. It skips `node_modules`, `.next` and build folders.
165
+
166
+ ### Code changes needed (errors)
167
+
168
+ Each of these fails the TypeScript build, or fails at runtime, with the developer preview:
169
+
170
+ | Rule | What it finds |
171
+ |---|---|
172
+ | `removed-export` | Imports of `DataClient`, `DefaultDataClient`, `EnhanceRouteOptions`, `GetRouteOptions`, `GetRouteFromMiddlewareOptions`, `GetRouteFromPageStateOptions`, `RewriteRouteOptions`, `RewriteRouteResult`, `expireMiddlewareCacheTag`, `precomputeComposition` (`@uniformdev/next-app-router`), `getRuleId`, `resolveComponentFromPageState`, `resolveRuleFromPageState`, `PageStateComponent`, `PageStateComponentFields` (`-shared`) and `ClientContextTestTransfer` (`-client`). |
173
+ | `removed-option` | The `dataClient` and `pathPatternsWithVariations` options and props. |
174
+ | `removed-config-key` | `middlewareRuntimeCache` and `experimental.disableSwrMiddlewareCache` in the server configuration. |
175
+ | `removed-page-state-field` | `pageState.components`, `.rules`, `.quirks`, `.isPrefetch` and `.requestPath`. |
176
+ | `result-code` | `.code` on the result of `resolveRouteFromCode` or `resolvePlaygroundRoute`. |
177
+ | `old-page-code` | Hard-coded `/uniform/2~…` page codes. |
178
+ | `next-version` | Next.js lower than 16.0.7. |
179
+ | `vercel-functions-transitive` | Imports of `@vercel/functions` when `package.json` does not list it. Version 20.82 installed it for you. |
180
+
181
+ ### Behavior changes (warnings)
182
+
183
+ | Rule | What it finds |
184
+ |---|---|
185
+ | `lite-mode-flicker` | `uniformMiddleware` or `handleUniformRoute`. The browser now chooses personalization and test variants after hydration. |
186
+ | `not-found-html` | All apps. The HTML of the 404 page is empty until hydration. |
187
+ | `route-union` | `route.compositionApiResponse` without a `route.type === "composition"` check. |
188
+ | `placement-props` | `PersonalizeProps` or `TestProps`. Their props changed. |
189
+ | `project-map-query-strings` | Project map nodes with query strings in `uniform-data`. |
190
+ | `visibility-rules` | Compositions with visibility rules in `uniform-data`. |
191
+ | `not-found-page` | `app/**/404/page.*` routes. The middleware no longer rewrites to `/404`. |
192
+ | `per-request-consent` | A `defaultConsent` value that is not a literal in the middleware. |
193
+ | `uniform-version-mismatch` | Different versions of `@uniformdev` packages. |
194
+ | `prerelease-peer-range` | `@uniformdev/toolbar-react` without an override for prerelease versions. |
195
+
196
+ ### Notes (info)
197
+
198
+ `cached-resolver`, `static-params`, `context-edge-dependency` and `redirect-status` tell you about changes that need no code change.
199
+
200
+ ## Limits
201
+
202
+ - The check reads the code. It does not run it. The check can miss a `pageState` with a different variable name, or a removed API that the code gets through a re-export.
203
+ - `uniform-data` is optional. Without it, the check cannot see query strings, visibility rules or personalizations in your content.
204
+ - To be sure, also install the developer preview in a branch and run `next build`.
205
+
206
+ ## Development
207
+
208
+ ```bash
209
+ npm install
210
+ npm test
211
+ node bin/cli.js test/fixtures/breaking-app
212
+ ```
213
+
214
+ ### Test the package on a local app before you publish
215
+
216
+ `npm pack` makes the same package that npm will publish. Use it to make sure that the package contains all the files that the CLI needs.
217
+
218
+ 1. Make the package:
219
+
220
+ ```bash
221
+ cd /path/to/the-cli-repo
222
+ npm pack
223
+ ```
224
+
225
+ The command makes the file `uniformdev-next-app-router-upgrade-<version>.tgz`.
226
+
227
+ 2. Go to the root folder of the app to check. This is the folder with the `package.json` of the app:
228
+
229
+ ```bash
230
+ cd /path/to/your-app
231
+ ```
232
+
233
+ 3. Run the CLI from the package:
234
+
235
+ ```bash
236
+ npx --package /path/to/the-cli-repo/uniformdev-next-app-router-upgrade-<version>.tgz uniform-next-app-router-upgrade
237
+ ```
238
+
239
+ Use the full path of the `.tgz` file, and give the command name after it. `npx <file>.tgz` without `--package` fails with "Permission denied".
240
+
241
+ 4. Read the exit code:
242
+
243
+ ```bash
244
+ echo $?
245
+ ```
246
+
247
+ After each change to the CLI, run `npm pack` again. If the version does not change, npx can use a copy from its cache. To prevent this, add `--prefer-online` to the `npx` command, or change the version in `package.json`.
248
+
249
+ The check only reads the app. It does not change files in the app. `.gitignore` excludes the `.tgz` files.
package/bin/cli.js ADDED
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { parseArgs } from "node:util";
6
+
7
+ import { PROVIDERS, providerNames } from "../src/edge/index.js";
8
+ import { runInteractive } from "../src/interactive.js";
9
+ import { planMigration } from "../src/migrate.js";
10
+ import { formatText, TARGET_VERSION, verdict } from "../src/report.js";
11
+ import { scanProject } from "../src/scan.js";
12
+ import { colors, executeMigration, NAME } from "../src/session.js";
13
+
14
+ const HOSTS = Object.entries(PROVIDERS)
15
+ .map(([key, provider]) => `${key}${provider.available ? "" : " (coming soon)"}`)
16
+ .join(", ");
17
+
18
+ const HELP = `Usage:
19
+ ${NAME} In a terminal: the interactive guide. Otherwise: check.
20
+ ${NAME} interactive [dir] The interactive guide through the check, phase 1 and phase 2.
21
+ ${NAME} check [dir] [options] Check if the app can move to the developer preview.
22
+ ${NAME} migrate [dir] [options] Phase 1: apply the safe code changes for the SDK upgrade.
23
+ ${NAME} migrate --edge <host> Phase 1 and phase 2: also turn on edge mode for a host.
24
+
25
+ The developer preview of @uniformdev/next-app-router is ${TARGET_VERSION}.
26
+
27
+ Options for check:
28
+ --json Print the result as JSON.
29
+ --strict Fail on warnings too.
30
+
31
+ Options for migrate:
32
+ --edge <host> Also turn on edge mode. Hosts: ${HOSTS}.
33
+ --dry-run Show the changes. Do not write files.
34
+ --yes Write the changes without a question.
35
+ --allow-dirty Write also when the files contain changes that are not committed, or when there is no git repository.
36
+
37
+ Options for all commands:
38
+ --uniform-data <dir> The folder of serialized Uniform content (default: uniform-data).
39
+ --no-color Print without colors.
40
+ -h, --help Show this help.
41
+
42
+ Exit codes:
43
+ check 0 safe to install, 1 code changes needed, 2 the check could not run.
44
+ migrate 0 no code changes left, 1 manual code changes left, 2 nothing was written because of an error or a safety check.`;
45
+
46
+ let args;
47
+ try {
48
+ args = parseArgs({
49
+ allowPositionals: true,
50
+ options: {
51
+ json: { type: "boolean" },
52
+ strict: { type: "boolean" },
53
+ "uniform-data": { type: "string" },
54
+ edge: { type: "string" },
55
+ "dry-run": { type: "boolean" },
56
+ yes: { type: "boolean", short: "y" },
57
+ "allow-dirty": { type: "boolean" },
58
+ "no-color": { type: "boolean" },
59
+ help: { type: "boolean", short: "h" },
60
+ },
61
+ });
62
+ } catch (error) {
63
+ console.error(error.message);
64
+ console.error(HELP);
65
+ process.exit(2);
66
+ }
67
+
68
+ if (args.values.help) {
69
+ console.log(HELP);
70
+ process.exit(0);
71
+ }
72
+
73
+ const COMMANDS = new Set(["check", "migrate", "interactive"]);
74
+ const positionals = [...args.positionals];
75
+ const terminal = Boolean(process.stdin.isTTY && process.stdout.isTTY);
76
+ let command = COMMANDS.has(positionals[0]) ? positionals.shift() : undefined;
77
+ command ??= terminal && !args.values.json && !args.values.strict ? "interactive" : "check";
78
+
79
+ const root = path.resolve(positionals[0] ?? ".");
80
+ const color = !args.values["no-color"] && process.stdout.isTTY;
81
+ const uniformData = args.values["uniform-data"];
82
+ const c = colors(color);
83
+
84
+ const edge = args.values.edge?.toLowerCase();
85
+ if (edge !== undefined) {
86
+ if (command !== "migrate") {
87
+ console.error("--edge works only with migrate.");
88
+ process.exit(2);
89
+ }
90
+ if (!PROVIDERS[edge]) {
91
+ console.error(`Unknown host "${edge}". Hosts: ${providerNames().join(", ")}.`);
92
+ process.exit(2);
93
+ }
94
+ if (!PROVIDERS[edge].available) {
95
+ console.error(`Edge mode for ${PROVIDERS[edge].name} is not in this CLI yet. To set it up by hand, follow the docs:`);
96
+ console.error(` ${PROVIDERS[edge].docs}`);
97
+ process.exit(2);
98
+ }
99
+ }
100
+
101
+ const scan = preflight(root);
102
+ if (command === "check") runCheck(scan);
103
+ else if (command === "interactive") {
104
+ if (!terminal) {
105
+ console.error("The interactive mode needs a terminal. Use check or migrate.");
106
+ process.exit(2);
107
+ }
108
+ process.exit(await runInteractive({ root, color, uniformData }));
109
+ } else {
110
+ console.log(c("1", "Uniform Next.js App Router SDK migration"));
111
+ console.log(`Target version: ${TARGET_VERSION}`);
112
+ console.log(`Project: ${root}`);
113
+ if (edge) console.log(`Edge mode: ${PROVIDERS[edge].name}`);
114
+ console.log("");
115
+ const ask = terminal ? confirmInTerminal : undefined;
116
+ const code = await executeMigration({
117
+ root,
118
+ edge,
119
+ dryRun: args.values["dry-run"],
120
+ yes: args.values.yes,
121
+ allowDirty: args.values["allow-dirty"],
122
+ color,
123
+ uniformData,
124
+ ask,
125
+ });
126
+ process.exit(code);
127
+ }
128
+
129
+ function preflight(dir) {
130
+ if (!fs.existsSync(path.join(dir, "package.json"))) {
131
+ console.error(`No package.json in ${dir}. Run the command in the root folder of your Next.js app.`);
132
+ process.exit(2);
133
+ }
134
+ const result = scanProject(dir, { uniformDataDir: uniformData });
135
+ if (result.project.usesV1 && !result.project.sdkVersion) {
136
+ console.error("This app uses v1 of the SDK (@uniformdev/canvas-next-rsc). Upgrade to v2 first:");
137
+ console.error("https://docs.uniform.app/docs/sdk/nextjs-app-router/upgrade-from-v1");
138
+ process.exit(2);
139
+ }
140
+ if (!result.project.sdkVersion) {
141
+ console.error("This app does not list @uniformdev/next-app-router in package.json.");
142
+ process.exit(2);
143
+ }
144
+ return result;
145
+ }
146
+
147
+ function runCheck(result) {
148
+ const outcome = verdict(result.findings, { strict: args.values.strict });
149
+ if (args.values.json) {
150
+ console.log(JSON.stringify({ targetVersion: TARGET_VERSION, verdict: outcome, ...result }, null, 2));
151
+ } else {
152
+ console.log(formatText(result, outcome, { color }));
153
+ const { changes } = planMigration(root);
154
+ if (changes.length) {
155
+ console.log("");
156
+ console.log(`${changes.length} change(s) can be applied automatically. To see them, run: ${NAME} migrate --dry-run`);
157
+ }
158
+ }
159
+ process.exit(outcome.safe ? 0 : 1);
160
+ }
161
+
162
+ async function confirmInTerminal(question, defaultYes = false) {
163
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
164
+ const answer = (await rl.question(`${question} ${defaultYes ? "[Y/n]" : "[y/N]"} `)).trim();
165
+ rl.close();
166
+ return answer ? /^y(es)?$/i.test(answer) : defaultYes;
167
+ }
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@uniformdev/next-app-router-upgrade",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Check and upgrade a Next.js App Router app to the developer preview of the Uniform SDK (@uniformdev/next-app-router), with edge mode.",
5
+ "keywords": [
6
+ "uniform",
7
+ "uniformdev",
8
+ "nextjs",
9
+ "app-router",
10
+ "upgrade",
11
+ "codemod",
12
+ "migration",
13
+ "edge"
14
+ ],
15
+ "type": "module",
16
+ "bin": {
17
+ "uniform-next-app-router-upgrade": "bin/cli.js"
18
+ },
19
+ "files": [
20
+ "bin",
21
+ "src",
22
+ "README.md"
23
+ ],
24
+ "engines": {
25
+ "node": ">=20.9.0"
26
+ },
27
+ "scripts": {
28
+ "test": "node --test test/*.test.js"
29
+ },
30
+ "dependencies": {
31
+ "typescript": "^5.6.0"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "license": "MIT"
37
+ }
package/src/apply.js ADDED
@@ -0,0 +1,124 @@
1
+ // Safety checks and file writes for the migration.
2
+
3
+ import { execFileSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ import { countSyntaxErrors } from "./migrate.js";
8
+
9
+ /**
10
+ * Returns the files of the plan that have uncommitted changes, or { notGit: true } when the
11
+ * project is not in a git repository.
12
+ */
13
+ export function gitState(root, files) {
14
+ try {
15
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root, stdio: "pipe" });
16
+ } catch {
17
+ return { notGit: true, dirty: [] };
18
+ }
19
+ const relative = files.map((file) => path.relative(root, file));
20
+ const out = execFileSync("git", ["status", "--porcelain", "--", ...relative], { cwd: root, encoding: "utf8" });
21
+ return { notGit: false, dirty: out.split("\n").filter(Boolean).map((line) => line.slice(3)) };
22
+ }
23
+
24
+ /** Returns the files where the new text has more syntax errors than the old text. */
25
+ export function validate(contents) {
26
+ const problems = [];
27
+ for (const [file, text] of contents) {
28
+ if (file.endsWith(".json")) {
29
+ try {
30
+ JSON.parse(text);
31
+ } catch (error) {
32
+ problems.push({ file, message: error.message });
33
+ }
34
+ continue;
35
+ }
36
+ const before = countSyntaxErrors(file, fs.readFileSync(file, "utf8"));
37
+ const after = countSyntaxErrors(file, text);
38
+ if (after > before) problems.push({ file, message: `${after - before} new syntax error(s)` });
39
+ }
40
+ return problems;
41
+ }
42
+
43
+ /**
44
+ * Writes all files, or none. Each file goes to a temporary file first, and then replaces the
45
+ * original. If a step fails, the original texts are written back.
46
+ */
47
+ export function writeAll(contents) {
48
+ const originals = new Map([...contents.keys()].map((file) => [file, fs.readFileSync(file, "utf8")]));
49
+ const temps = [];
50
+ try {
51
+ for (const [file, text] of contents) {
52
+ const temp = `${file}.uniform-migrate-${process.pid}.tmp`;
53
+ fs.writeFileSync(temp, text, { mode: fs.statSync(file).mode });
54
+ temps.push([temp, file]);
55
+ }
56
+ const done = [];
57
+ try {
58
+ for (const [temp, file] of temps) {
59
+ fs.renameSync(temp, file);
60
+ done.push(file);
61
+ }
62
+ } catch (error) {
63
+ for (const file of done) fs.writeFileSync(file, originals.get(file));
64
+ throw error;
65
+ }
66
+ } finally {
67
+ for (const [temp] of temps) fs.rmSync(temp, { force: true });
68
+ }
69
+ }
70
+
71
+ /** A unified diff with 2 lines of context, for the dry run. */
72
+ export function unifiedDiff(name, before, after, { color = false } = {}) {
73
+ const a = before.split("\n");
74
+ const b = after.split("\n");
75
+ const c = (code, s) => (color ? `\u001b[${code}m${s}\u001b[0m` : s);
76
+
77
+ // Longest common subsequence of the lines. The files of an app are small enough for this.
78
+ const n = a.length;
79
+ const m = b.length;
80
+ const lcs = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
81
+ for (let i = n - 1; i >= 0; i--) {
82
+ for (let j = m - 1; j >= 0; j--) lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
83
+ }
84
+ const ops = [];
85
+ let i = 0;
86
+ let j = 0;
87
+ while (i < n || j < m) {
88
+ if (i < n && j < m && a[i] === b[j]) ops.push({ type: " ", line: a[i], i: i++, j: j++ });
89
+ else if (i < n && (j === m || lcs[i + 1][j] >= lcs[i][j + 1])) ops.push({ type: "-", line: a[i], i: i++, j });
90
+ else ops.push({ type: "+", line: b[j], i, j: j++ });
91
+ }
92
+
93
+ const CONTEXT = 2;
94
+ const out = [c("1", `--- ${name}`), c("1", `+++ ${name}`)];
95
+ let k = 0;
96
+ while (k < ops.length) {
97
+ if (ops[k].type === " ") {
98
+ k++;
99
+ continue;
100
+ }
101
+ let start = Math.max(0, k - CONTEXT);
102
+ let end = k;
103
+ // Extend the hunk while the next change is near.
104
+ while (end < ops.length) {
105
+ if (ops[end].type !== " ") {
106
+ end++;
107
+ continue;
108
+ }
109
+ let next = end;
110
+ while (next < ops.length && ops[next].type === " ") next++;
111
+ if (next < ops.length && next - end <= CONTEXT * 2) end = next;
112
+ else break;
113
+ }
114
+ end = Math.min(ops.length, end + CONTEXT);
115
+ const first = ops[start];
116
+ out.push(c("36", `@@ -${first.i + 1} +${first.j + 1} @@`));
117
+ for (const op of ops.slice(start, end)) {
118
+ const line = `${op.type}${op.line}`;
119
+ out.push(op.type === "+" ? c("32", line) : op.type === "-" ? c("31", line) : line);
120
+ }
121
+ k = end;
122
+ }
123
+ return out.join("\n");
124
+ }
@@ -0,0 +1,37 @@
1
+ // Phase 2: edge mode for each host. Add a provider here when its migration is ready.
2
+
3
+ import { planVercelEdge } from "./vercel.js";
4
+
5
+ const DOCS = "https://docs.uniform.app/docs/sdk/nextjs-app-router/edge-mode";
6
+
7
+ export const PROVIDERS = {
8
+ vercel: {
9
+ name: "Vercel",
10
+ available: true,
11
+ summary: "vercelUniformEdgeMiddleware, the Vercel runtime cache and per-visitor ETags.",
12
+ docs: `${DOCS}#set-up-edge-mode-on-vercel`,
13
+ plan: planVercelEdge,
14
+ },
15
+ cloudflare: {
16
+ name: "Cloudflare",
17
+ available: false,
18
+ summary: "Coming soon.",
19
+ docs: `${DOCS}#set-up-edge-mode-on-other-hosts`,
20
+ },
21
+ netlify: {
22
+ name: "Netlify",
23
+ available: false,
24
+ summary: "Coming soon.",
25
+ docs: `${DOCS}#set-up-edge-mode-on-other-hosts`,
26
+ },
27
+ akamai: {
28
+ name: "Akamai",
29
+ available: false,
30
+ summary: "Coming soon.",
31
+ docs: `${DOCS}#set-up-edge-mode-on-other-hosts`,
32
+ },
33
+ };
34
+
35
+ export function providerNames() {
36
+ return Object.keys(PROVIDERS);
37
+ }