next-evolve 0.1.0 → 0.3.1
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 +55 -1
- package/dist/_virtual/_rolldown/runtime.js +12 -0
- package/dist/aggregate/attribution.d.ts +40 -0
- package/dist/aggregate/attribution.js +115 -0
- package/dist/aggregate/run.d.ts +21 -0
- package/dist/aggregate/run.js +139 -0
- package/dist/cli/aggregate.d.ts +9 -0
- package/dist/cli/aggregate.js +104 -0
- package/dist/cli/args.d.ts +12 -0
- package/dist/cli/args.js +23 -0
- package/dist/cli/cloud.d.ts +41 -0
- package/dist/cli/cloud.js +74 -0
- package/dist/cli/connect.d.ts +8 -0
- package/dist/cli/connect.js +46 -0
- package/dist/cli/detect.d.ts +26 -0
- package/dist/cli/detect.js +97 -0
- package/dist/cli/doctor.d.ts +34 -0
- package/dist/cli/doctor.js +230 -0
- package/dist/cli/env-file.d.ts +4 -0
- package/dist/cli/env-file.js +15 -0
- package/dist/cli/files.d.ts +25 -0
- package/dist/cli/files.js +173 -0
- package/dist/cli/index.js +73 -32
- package/dist/cli/init.d.ts +26 -0
- package/dist/cli/init.js +240 -0
- package/dist/cli/load-app.d.ts +19 -0
- package/dist/cli/load-app.js +67 -0
- package/dist/cli/migrate.d.ts +7 -0
- package/dist/cli/migrate.js +30 -0
- package/dist/cli/pages.d.ts +17 -0
- package/dist/cli/pages.js +304 -0
- package/dist/cli/plan.d.ts +27 -0
- package/dist/cli/plan.js +129 -0
- package/dist/cli/pull.d.ts +13 -0
- package/dist/cli/pull.js +62 -0
- package/dist/cli/push.d.ts +14 -0
- package/dist/cli/push.js +33 -0
- package/dist/cloud/manifest.d.ts +2 -0
- package/dist/cloud/manifest.js +6 -2
- package/dist/core/assign.d.ts +13 -1
- package/dist/core/assign.js +25 -3
- package/dist/db/writer.d.ts +2 -0
- package/dist/db/writer.js +1 -1
- package/dist/instrumentation/index.d.ts +3 -2
- package/dist/instrumentation/index.js +17 -2
- package/dist/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/typescript.d.ts +11169 -0
- package/dist/proxy/index.d.ts +2 -2
- package/dist/proxy/index.js +1 -1
- package/dist/proxy/proxy.d.ts +7 -1
- package/dist/proxy/proxy.js +23 -6
- package/dist/react/render-bundle.d.ts +10 -4
- package/dist/react/render-bundle.js +34 -5
- package/dist/server/aggregate-route.d.ts +16 -0
- package/dist/server/aggregate-route.js +68 -0
- package/dist/server/collect.d.ts +16 -0
- package/dist/server/collect.js +34 -8
- package/dist/server/handler.d.ts +3 -2
- package/dist/server/handler.js +37 -4
- package/dist/server/sync-route.d.ts +28 -0
- package/dist/server/sync-route.js +100 -0
- package/dist/store/disk.d.ts +6 -0
- package/dist/store/disk.js +23 -0
- package/dist/store/fetch.d.ts +4 -0
- package/dist/store/fetch.js +36 -1
- package/dist/store/global-config.d.ts +39 -0
- package/dist/store/global-config.js +122 -0
- package/dist/store/memory.d.ts +11 -2
- package/dist/store/memory.js +6 -2
- package/dist/store/runtime.d.ts +14 -0
- package/dist/store/runtime.js +39 -7
- package/package.json +2 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { parseFlags } from "./args.js";
|
|
2
|
+
import { getSiteInfo, putAppUrl, runCloudTest } from "./cloud.js";
|
|
3
|
+
import { parseSecretKey } from "@next-evolve/protocol";
|
|
4
|
+
// src/cli/connect.ts
|
|
5
|
+
async function connectCommand(args, deps) {
|
|
6
|
+
const out = deps.out ?? ((s) => console.warn(s));
|
|
7
|
+
const err = deps.err ?? ((s) => console.error(s));
|
|
8
|
+
const p = parseFlags(args, {
|
|
9
|
+
test: { type: "boolean" },
|
|
10
|
+
"app-url": { type: "string" }
|
|
11
|
+
});
|
|
12
|
+
if (!p.ok) {
|
|
13
|
+
err(`next-evolve connect: ${p.error}`);
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
const key = deps.env.EVOLVE_SECRET_KEY ?? "";
|
|
17
|
+
if (!parseSecretKey(key)) {
|
|
18
|
+
err("next-evolve connect: set EVOLVE_SECRET_KEY (evk_live_…)");
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
const site = await getSiteInfo(deps, key);
|
|
22
|
+
if (!site.ok) {
|
|
23
|
+
err(`next-evolve connect: ${site.error}`);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
out(`✓ Connected to "${site.site.name}" (${site.site.id})`);
|
|
27
|
+
if (p.flags["app-url"]) {
|
|
28
|
+
const r = await putAppUrl(deps, key, p.flags["app-url"]);
|
|
29
|
+
if (!r.ok) {
|
|
30
|
+
err(`✗ App URL: ${r.error}`);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
out(`✓ App URL: ${r.appUrl}`);
|
|
34
|
+
} else if (site.site.appUrl) out(` App URL: ${site.site.appUrl}`);
|
|
35
|
+
if (!p.flags.test) return 0;
|
|
36
|
+
const t = await runCloudTest(deps, key);
|
|
37
|
+
if (!t.ok) {
|
|
38
|
+
err(`✗ Connection test: ${t.error}`);
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
const line = (name, c) => `${c.ok ? "✓" : "✗"} ${name}${c.detail ? ` (${c.detail})` : ""}`;
|
|
42
|
+
out(line("Sync webhook reaches the app", t.webhook));
|
|
43
|
+
out(line("Aggregate route answers (signed dry run)", t.aggregate));
|
|
44
|
+
return t.webhook.ok && t.aggregate.ok ? 0 : 1;
|
|
45
|
+
}
|
|
46
|
+
export { connectCommand };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type PackageManager = "pnpm" | "yarn" | "npm" | "bun";
|
|
2
|
+
export type Problem = {
|
|
3
|
+
level: "error" | "warn";
|
|
4
|
+
message: string;
|
|
5
|
+
};
|
|
6
|
+
export type Detection = {
|
|
7
|
+
root: string;
|
|
8
|
+
/** Installed version, else the declared one (range stripped). */
|
|
9
|
+
nextVersion: string | null;
|
|
10
|
+
/** `app` or `src/app`. */
|
|
11
|
+
appDir: string | null;
|
|
12
|
+
srcDir: boolean;
|
|
13
|
+
typescript: boolean;
|
|
14
|
+
packageManager: PackageManager;
|
|
15
|
+
/** Existing proxy/middleware file, relative to root. */
|
|
16
|
+
proxyFile: string | null;
|
|
17
|
+
/** What Next calls it: `proxy.ts` from 16, `middleware.ts` (runtime nodejs) on 15.5. */
|
|
18
|
+
proxyKind: "proxy" | "middleware";
|
|
19
|
+
nextConfigFile: string | null;
|
|
20
|
+
outputExport: boolean;
|
|
21
|
+
cacheComponents: boolean;
|
|
22
|
+
hasEvolve: boolean;
|
|
23
|
+
hasZod: boolean;
|
|
24
|
+
problems: Problem[];
|
|
25
|
+
};
|
|
26
|
+
export declare function detectProject(root: string): Detection;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
// src/cli/detect.ts
|
|
4
|
+
const read = (p) => {
|
|
5
|
+
try {
|
|
6
|
+
return readFileSync(p, "utf8");
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
function parseVersion(v) {
|
|
12
|
+
const m = /(\d+)\.(\d+)/.exec(v ?? "");
|
|
13
|
+
return m ? [Number(m[1]), Number(m[2])] : null;
|
|
14
|
+
}
|
|
15
|
+
function detectProject(root) {
|
|
16
|
+
const problems = [];
|
|
17
|
+
const pkgText = read(join(root, "package.json"));
|
|
18
|
+
let pkg = {};
|
|
19
|
+
try {
|
|
20
|
+
pkg = pkgText ? JSON.parse(pkgText) : {};
|
|
21
|
+
} catch {
|
|
22
|
+
problems.push({
|
|
23
|
+
level: "error",
|
|
24
|
+
message: "package.json isn't valid JSON"
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const deps = {
|
|
28
|
+
...pkg.devDependencies,
|
|
29
|
+
...pkg.dependencies
|
|
30
|
+
};
|
|
31
|
+
const installed = read(join(root, "node_modules/next/package.json"));
|
|
32
|
+
let nextVersion = null;
|
|
33
|
+
try {
|
|
34
|
+
nextVersion = installed ? JSON.parse(installed).version : null;
|
|
35
|
+
} catch {}
|
|
36
|
+
if (!nextVersion && deps.next) {
|
|
37
|
+
const v = parseVersion(deps.next);
|
|
38
|
+
nextVersion = v ? `${v[0]}.${v[1]}.0` : deps.next;
|
|
39
|
+
}
|
|
40
|
+
const ver = parseVersion(nextVersion);
|
|
41
|
+
const appDir = existsSync(join(root, "src/app")) ? "src/app" : existsSync(join(root, "app")) ? "app" : null;
|
|
42
|
+
const srcDir = appDir === "src/app";
|
|
43
|
+
const packageManager = existsSync(join(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync(join(root, "yarn.lock")) ? "yarn" : existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb")) ? "bun" : "npm";
|
|
44
|
+
const base = srcDir ? "src/" : "";
|
|
45
|
+
const proxyFile = ["proxy", "middleware"].flatMap((n) => [
|
|
46
|
+
"ts",
|
|
47
|
+
"js",
|
|
48
|
+
"mjs",
|
|
49
|
+
"tsx"
|
|
50
|
+
].map((e) => `${base}${n}.${e}`)).find((f) => existsSync(join(root, f))) ?? null;
|
|
51
|
+
const nextConfigFile = [
|
|
52
|
+
"next.config.ts",
|
|
53
|
+
"next.config.mjs",
|
|
54
|
+
"next.config.js",
|
|
55
|
+
"next.config.cjs"
|
|
56
|
+
].find((f) => existsSync(join(root, f))) ?? null;
|
|
57
|
+
const nextConfig = nextConfigFile ? read(join(root, nextConfigFile)) ?? "" : "";
|
|
58
|
+
const outputExport = /output\s*:\s*["'`]export["'`]/.test(nextConfig);
|
|
59
|
+
const cacheComponents = /cacheComponents\s*:\s*true/.test(nextConfig);
|
|
60
|
+
if (!deps.next && !installed) problems.push({
|
|
61
|
+
level: "error",
|
|
62
|
+
message: "This isn't a Next.js app (no next in package.json)"
|
|
63
|
+
});
|
|
64
|
+
else if (!ver || ver[0] < 15 || ver[0] === 15 && ver[1] < 5) problems.push({
|
|
65
|
+
level: "error",
|
|
66
|
+
message: `Evolve needs Next.js 15.5 or newer (found ${nextVersion ?? "unknown"})`
|
|
67
|
+
});
|
|
68
|
+
if (deps.next && !appDir) problems.push({
|
|
69
|
+
level: "error",
|
|
70
|
+
message: "Evolve needs the App Router (no app/ directory)"
|
|
71
|
+
});
|
|
72
|
+
if (outputExport) problems.push({
|
|
73
|
+
level: "error",
|
|
74
|
+
message: "output: \"export\" builds a static site; Evolve needs a server (remove it)"
|
|
75
|
+
});
|
|
76
|
+
if (cacheComponents) problems.push({
|
|
77
|
+
level: "warn",
|
|
78
|
+
message: "cacheComponents is on: static-mode pages are untested with it; prefer dynamic mode"
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
root,
|
|
82
|
+
nextVersion,
|
|
83
|
+
appDir,
|
|
84
|
+
srcDir,
|
|
85
|
+
typescript: existsSync(join(root, "tsconfig.json")),
|
|
86
|
+
packageManager,
|
|
87
|
+
proxyFile,
|
|
88
|
+
proxyKind: ver && ver[0] >= 16 ? "proxy" : "middleware",
|
|
89
|
+
nextConfigFile,
|
|
90
|
+
outputExport,
|
|
91
|
+
cacheComponents,
|
|
92
|
+
hasEvolve: Boolean(deps["next-evolve"]),
|
|
93
|
+
hasZod: Boolean(deps.zod),
|
|
94
|
+
problems
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export { detectProject };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AppModules } from "./load-app.js";
|
|
2
|
+
type Env = Record<string, string | undefined>;
|
|
3
|
+
export type Check = {
|
|
4
|
+
name: string;
|
|
5
|
+
status: "ok" | "warn" | "fail";
|
|
6
|
+
detail?: string;
|
|
7
|
+
};
|
|
8
|
+
export type DoctorDeps = {
|
|
9
|
+
env: Env;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
out?: (s: string) => void;
|
|
13
|
+
err?: (s: string) => void;
|
|
14
|
+
/** Milliseconds (tests move the clock). */
|
|
15
|
+
now?: () => number;
|
|
16
|
+
/** The app's config (tests); default loads evolve.config. */
|
|
17
|
+
load?: () => Promise<AppModules>;
|
|
18
|
+
/** Reachable and migrated? Default connects with the app's DB URL. */
|
|
19
|
+
checkDb?: (url: string) => Promise<{
|
|
20
|
+
ok: true;
|
|
21
|
+
pending: string[];
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
error: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** Override the app URL the cloud has (tests); default from GET /v1/site. */
|
|
27
|
+
appUrl?: string | null;
|
|
28
|
+
};
|
|
29
|
+
/** The string literals in `config.matcher`, or null when there's no matcher (= every path). */
|
|
30
|
+
export declare function readMatcher(src: string): string[] | null;
|
|
31
|
+
export declare const covers: (matchers: string[] | null, path: string) => boolean;
|
|
32
|
+
export declare function runDoctor(deps: DoctorDeps): Promise<Check[]>;
|
|
33
|
+
export declare function doctorCommand(args: string[], deps: DoctorDeps): Promise<number>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { normalizePath } from "../config/config.js";
|
|
2
|
+
import { trustedKeys } from "../core/keys.js";
|
|
3
|
+
import { createSql } from "../db/client.js";
|
|
4
|
+
import { loadMigrations } from "../db/migrate.js";
|
|
5
|
+
import { fetchBundle } from "../store/fetch.js";
|
|
6
|
+
import { cloudUrl, getSiteInfo, runCloudTest } from "./cloud.js";
|
|
7
|
+
import { detectProject } from "./detect.js";
|
|
8
|
+
import { fileEnv } from "./env-file.js";
|
|
9
|
+
import { loadAppModules, reexecWithTypes } from "./load-app.js";
|
|
10
|
+
import { BUILD_BUNDLE_PATH, readBuildBundle } from "./pull.js";
|
|
11
|
+
import { parseSecretKey, verifyBundle } from "@next-evolve/protocol";
|
|
12
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
// src/cli/doctor.ts
|
|
15
|
+
const DAY = 86400;
|
|
16
|
+
const ok = (name, detail) => ({
|
|
17
|
+
name,
|
|
18
|
+
status: "ok",
|
|
19
|
+
detail
|
|
20
|
+
});
|
|
21
|
+
const warn = (name, detail) => ({
|
|
22
|
+
name,
|
|
23
|
+
status: "warn",
|
|
24
|
+
detail
|
|
25
|
+
});
|
|
26
|
+
const fail = (name, detail) => ({
|
|
27
|
+
name,
|
|
28
|
+
status: "fail",
|
|
29
|
+
detail
|
|
30
|
+
});
|
|
31
|
+
/** Next matcher (path-to-regexp style) → RegExp; null when it can't be read. */
|
|
32
|
+
function matcherRegex(m) {
|
|
33
|
+
try {
|
|
34
|
+
const s = m.replace(/\/:\w+\*/g, "(?:/.*)?").replace(/\/:\w+\+/g, "(?:/.+)").replace(/\/:\w+\?/g, "(?:/[^/]+)?").replace(/:\w+/g, "[^/]+");
|
|
35
|
+
return new RegExp(`^${s}$`);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** The string literals in `config.matcher`, or null when there's no matcher (= every path). */
|
|
41
|
+
function readMatcher(src) {
|
|
42
|
+
const m = /matcher\s*:\s*(\[[\s\S]*?\]|"[^"]*"|'[^']*'|`[^`]*`)/.exec(src);
|
|
43
|
+
if (!m) return null;
|
|
44
|
+
return [...m[1].matchAll(/(["'`])((?:\\.|(?!\1).)*)\1/g)].map((x) => x[2]);
|
|
45
|
+
}
|
|
46
|
+
const covers = (matchers, path) => matchers === null || matchers.some((m) => m === path || matcherRegex(m)?.test(path));
|
|
47
|
+
function dirs(root, dir) {
|
|
48
|
+
try {
|
|
49
|
+
return readdirSync(join(root, dir)).filter((e) => statSync(join(root, dir, e)).isDirectory());
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const read = (p) => {
|
|
55
|
+
try {
|
|
56
|
+
return readFileSync(p, "utf8");
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
async function runDoctor(deps) {
|
|
62
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
63
|
+
const env = {
|
|
64
|
+
...fileEnv(cwd),
|
|
65
|
+
...deps.env
|
|
66
|
+
};
|
|
67
|
+
const nowMs = (deps.now ?? Date.now)();
|
|
68
|
+
const checks = [];
|
|
69
|
+
const d = detectProject(cwd);
|
|
70
|
+
const errors = d.problems.filter((p) => p.level === "error");
|
|
71
|
+
const warns = d.problems.filter((p) => p.level === "warn");
|
|
72
|
+
checks.push(errors.length ? fail("Next.js", errors.map((p) => p.message).join("; ")) : warns.length ? warn("Next.js", warns.map((p) => p.message).join("; ")) : ok("Next.js", `${d.nextVersion} · ${d.appDir}/`));
|
|
73
|
+
const required = ["EVOLVE_SECRET_KEY", "EVOLVE_WEBHOOK_SECRET"].filter((k) => !env[k]);
|
|
74
|
+
const optional = ["EVOLVE_DATABASE_URL", "EVOLVE_TRACK_SECRET"].filter((k) => !env[k]);
|
|
75
|
+
checks.push(required.length ? fail("Env vars", `missing ${required.join(", ")}`) : optional.length ? warn("Env vars", `missing ${optional.join(", ")}`) : ok("Env vars"));
|
|
76
|
+
const key = env.EVOLVE_SECRET_KEY ?? "";
|
|
77
|
+
const parsed = parseSecretKey(key);
|
|
78
|
+
let appUrl = deps.appUrl === void 0 ? null : deps.appUrl;
|
|
79
|
+
let keyOk = false;
|
|
80
|
+
if (!parsed) checks.push(fail("Secret key", "EVOLVE_SECRET_KEY isn't an evk_live_… key"));
|
|
81
|
+
else {
|
|
82
|
+
const site = await getSiteInfo({
|
|
83
|
+
env,
|
|
84
|
+
fetch: deps.fetch
|
|
85
|
+
}, key);
|
|
86
|
+
if (!site.ok) checks.push(fail("Secret key", site.error));
|
|
87
|
+
else {
|
|
88
|
+
keyOk = true;
|
|
89
|
+
if (deps.appUrl === void 0) appUrl = site.site.appUrl;
|
|
90
|
+
checks.push(ok("Secret key", `"${site.site.name}" (${site.site.id})`));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const dbUrl = env.EVOLVE_DATABASE_URL;
|
|
94
|
+
if (!dbUrl) checks.push(warn("Database", "EVOLVE_DATABASE_URL isn't set: variants serve, nothing is measured"));
|
|
95
|
+
else {
|
|
96
|
+
const r = await (deps.checkDb ?? checkDatabase)(dbUrl);
|
|
97
|
+
checks.push(!r.ok ? fail("Database", r.error) : r.pending.length ? fail("Database", `not migrated (${r.pending.join(", ")}): run npx next-evolve migrate`) : ok("Database", "reachable and migrated"));
|
|
98
|
+
}
|
|
99
|
+
const now = Math.floor(nowMs / 1e3);
|
|
100
|
+
if (!keyOk || !parsed) checks.push(fail("Bundle", "needs a working secret key"));
|
|
101
|
+
else {
|
|
102
|
+
const res = await fetchBundle({
|
|
103
|
+
cloudUrl: cloudUrl(env),
|
|
104
|
+
secretKey: key,
|
|
105
|
+
timeoutMs: 1e4,
|
|
106
|
+
fetch: deps.fetch
|
|
107
|
+
});
|
|
108
|
+
if (res.status !== "ok") checks.push(fail("Bundle", "couldn't fetch the bundle"));
|
|
109
|
+
else {
|
|
110
|
+
const v = verifyBundle(res.token, {
|
|
111
|
+
keys: trustedKeys(env),
|
|
112
|
+
site: parsed.siteId,
|
|
113
|
+
now
|
|
114
|
+
});
|
|
115
|
+
const days = v.ok ? Math.floor((v.payload.exp - now) / DAY) : 0;
|
|
116
|
+
const build = read(join(cwd, BUILD_BUNDLE_PATH));
|
|
117
|
+
if (!v.ok) checks.push(fail("Bundle", `rejected (${v.reason})`));
|
|
118
|
+
else if (v.payload.exp - now < 7 * DAY) checks.push(fail("Bundle", `expires in ${days} day(s); the cloud re-signs daily, so check its cron/status`));
|
|
119
|
+
else if (!build || !readBuildBundle(build)) checks.push(warn("Bundle", `no ${BUILD_BUNDLE_PATH}: add "prebuild": "next-evolve pull" (cold starts serve control)`));
|
|
120
|
+
else checks.push(ok("Bundle", `ver ${v.payload.ver}, expires in ${days} days`));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
let config = null;
|
|
124
|
+
let configError = null;
|
|
125
|
+
try {
|
|
126
|
+
if (deps.load) config = (await deps.load()).config;
|
|
127
|
+
else {
|
|
128
|
+
const r = await loadAppModules(cwd);
|
|
129
|
+
if (r.ok) config = r.mods.config;
|
|
130
|
+
else configError = r.error;
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
configError = e instanceof Error ? e.message : String(e);
|
|
134
|
+
}
|
|
135
|
+
const routes = config ? Object.entries(config.routes ?? {}).map(([p, r]) => ({
|
|
136
|
+
path: normalizePath(p),
|
|
137
|
+
mode: r.mode
|
|
138
|
+
})) : [];
|
|
139
|
+
if (!d.proxyFile) checks.push(fail("Proxy matcher", `no proxy (${d.proxyKind}.ts) file`));
|
|
140
|
+
else {
|
|
141
|
+
const src = read(join(cwd, d.proxyFile)) ?? "";
|
|
142
|
+
if (!src.includes("next-evolve")) checks.push(fail("Proxy matcher", `${d.proxyFile} doesn't run Evolve (applyEvolve)`));
|
|
143
|
+
else {
|
|
144
|
+
const matchers = readMatcher(src);
|
|
145
|
+
const missing = [...routes.filter((r) => !covers(matchers, r.path)).map((r) => r.path), ...covers(matchers, "/ev_0") ? [] : ["/(ev_.*)"]];
|
|
146
|
+
checks.push(missing.length ? fail("Proxy matcher", `config.matcher in ${d.proxyFile} misses ${missing.join(", ")}`) : ok("Proxy matcher"));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const app = d.appDir ?? "app";
|
|
150
|
+
if (!config) checks.push(fail("Pages", configError ?? "couldn't load evolve.config"));
|
|
151
|
+
else {
|
|
152
|
+
const problems = [];
|
|
153
|
+
const statics = routes.filter((r) => r.mode === "static");
|
|
154
|
+
const clash = dirs(cwd, app).find((x) => /^\[.+\]$/.test(x) && x !== "[evolve]");
|
|
155
|
+
if (statics.length && clash) problems.push(`${app}/${clash} conflicts with ${app}/[evolve]; use "dynamic" mode`);
|
|
156
|
+
for (const r of statics) {
|
|
157
|
+
const seg = r.path === "/" ? "" : r.path;
|
|
158
|
+
const page = [
|
|
159
|
+
"tsx",
|
|
160
|
+
"jsx",
|
|
161
|
+
"ts",
|
|
162
|
+
"js"
|
|
163
|
+
].map((e) => `${app}/[evolve]${seg}/page.${e}`).find((f) => existsSync(join(cwd, f)));
|
|
164
|
+
if (!page) problems.push(`${r.path}: no ${app}/[evolve]${seg}/page (run init again)`);
|
|
165
|
+
else if (!read(join(cwd, page))?.includes("withEvolve")) problems.push(`${r.path}: ${page} isn't wrapped with withEvolve()`);
|
|
166
|
+
}
|
|
167
|
+
checks.push(problems.length ? fail("Pages", problems.join("; ")) : ok("Pages"));
|
|
168
|
+
}
|
|
169
|
+
const layout = [
|
|
170
|
+
"tsx",
|
|
171
|
+
"jsx",
|
|
172
|
+
"js",
|
|
173
|
+
"ts"
|
|
174
|
+
].map((e) => `${app}/layout.${e}`).find((f) => existsSync(join(cwd, f)));
|
|
175
|
+
checks.push(layout && read(join(cwd, layout))?.includes("EvolveScript") ? ok("EvolveScript") : fail("EvolveScript", `add <EvolveScript /> (next-evolve/client) to ${layout ?? "the root layout"}`));
|
|
176
|
+
if (!keyOk) checks.push(fail("Cloud → app", "needs a working secret key"));
|
|
177
|
+
else if (!appUrl) checks.push(warn("Cloud → app", "no app URL yet; after you deploy: npx next-evolve connect --test --app-url https://…"));
|
|
178
|
+
else {
|
|
179
|
+
const t = await runCloudTest({
|
|
180
|
+
env,
|
|
181
|
+
fetch: deps.fetch
|
|
182
|
+
}, key);
|
|
183
|
+
if (!t.ok) checks.push(fail("Cloud → app", t.error));
|
|
184
|
+
else {
|
|
185
|
+
const bad = [...t.webhook.ok ? [] : [`sync route: ${t.webhook.detail ?? "failed"}`], ...t.aggregate.ok ? [] : [`aggregate route: ${t.aggregate.detail ?? "failed"}`]];
|
|
186
|
+
checks.push(bad.length ? fail("Cloud → app", `${appUrl}: ${bad.join("; ")}`) : ok("Cloud → app", appUrl));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const onVercel = Boolean(env.VERCEL) || existsSync(join(cwd, ".vercel/project.json"));
|
|
190
|
+
checks.push(onVercel ? ok("CDN", "Vercel") : warn("CDN", "not on Vercel: if a CDN caches pages, it must not cache Evolve routes by URL alone (the variant comes from the evolve cookie)"));
|
|
191
|
+
return checks;
|
|
192
|
+
}
|
|
193
|
+
async function checkDatabase(url) {
|
|
194
|
+
const sql = createSql(url, { max: 1 });
|
|
195
|
+
try {
|
|
196
|
+
await sql`select 1`;
|
|
197
|
+
const [t] = await sql`select to_regclass('evolve.migrations') as r`;
|
|
198
|
+
const done = t?.r ? new Set((await sql`select id from evolve.migrations`).map((r) => r.id)) : /* @__PURE__ */ new Set();
|
|
199
|
+
return {
|
|
200
|
+
ok: true,
|
|
201
|
+
pending: (await loadMigrations()).map((m) => m.id).filter((id) => !done.has(id))
|
|
202
|
+
};
|
|
203
|
+
} catch (e) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
error: e instanceof Error ? e.message : String(e)
|
|
207
|
+
};
|
|
208
|
+
} finally {
|
|
209
|
+
await sql.end({ timeout: 5 }).catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function doctorCommand(args, deps) {
|
|
213
|
+
const out = deps.out ?? ((s) => console.warn(s));
|
|
214
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
215
|
+
if (!deps.load) {
|
|
216
|
+
const r = await loadAppModules(cwd);
|
|
217
|
+
if (!r.ok && r.needsTs && process.argv[1]) return reexecWithTypes("doctor", args, cwd, deps.env);
|
|
218
|
+
}
|
|
219
|
+
const checks = await runDoctor(deps);
|
|
220
|
+
const mark = {
|
|
221
|
+
ok: "✓",
|
|
222
|
+
warn: "!",
|
|
223
|
+
fail: "✗"
|
|
224
|
+
};
|
|
225
|
+
for (const c of checks) out(`${mark[c.status]} ${c.name}${c.detail ? `: ${c.detail}` : ""}`);
|
|
226
|
+
const failed = checks.filter((c) => c.status === "fail").length;
|
|
227
|
+
out(failed ? `\n${failed} check(s) failed.` : "\nAll good.");
|
|
228
|
+
return failed ? 1 : 0;
|
|
229
|
+
}
|
|
230
|
+
export { covers, doctorCommand, readMatcher, runDoctor };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
// src/cli/env-file.ts
|
|
4
|
+
/** Reads a value from the app's .env.local / .env without touching process.env. */
|
|
5
|
+
function fileEnv(root) {
|
|
6
|
+
const out = {};
|
|
7
|
+
for (const f of [".env", ".env.local"]) try {
|
|
8
|
+
for (const line of readFileSync(join(root, f), "utf8").split("\n")) {
|
|
9
|
+
const m = /^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$/.exec(line);
|
|
10
|
+
if (m) out[m[1]] = m[2].replace(/^(['"])(.*)\1$/, "$2");
|
|
11
|
+
}
|
|
12
|
+
} catch {}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
export { fileEnv };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Detection } from "./detect.js";
|
|
2
|
+
import { Plan } from "./plan.js";
|
|
3
|
+
export type RouteMode = {
|
|
4
|
+
path: string;
|
|
5
|
+
mode: "static" | "dynamic";
|
|
6
|
+
};
|
|
7
|
+
/** Import specifier from one project file to another (extension dropped). */
|
|
8
|
+
export declare function relImport(fromFile: string, toFile: string): string;
|
|
9
|
+
export declare const configFile: (d: Detection) => string;
|
|
10
|
+
export declare const goalsFile: (d: Detection) => string;
|
|
11
|
+
export declare const routeFile: (d: Detection) => string;
|
|
12
|
+
export declare const matcherFor: (routes: RouteMode[]) => string[];
|
|
13
|
+
export declare function planConfig(plan: Plan, d: Detection, routes: RouteMode[]): void;
|
|
14
|
+
export declare function planGoals(plan: Plan, d: Detection): void;
|
|
15
|
+
export declare function planRouteHandler(plan: Plan, d: Detection): void;
|
|
16
|
+
export declare function planProxy(plan: Plan, d: Detection, routes: RouteMode[]): void;
|
|
17
|
+
/** Imports go after the last import line (or at the top). */
|
|
18
|
+
export declare function addImport(src: string, line: string): string;
|
|
19
|
+
export declare function planLayout(plan: Plan, d: Detection): void;
|
|
20
|
+
export declare function planInstrumentation(plan: Plan, d: Detection): void;
|
|
21
|
+
export declare function planPackageJson(plan: Plan): void;
|
|
22
|
+
export declare function planGitignore(plan: Plan): void;
|
|
23
|
+
export declare const newTrackSecret: () => string;
|
|
24
|
+
/** Appends variables missing from .env.local; never changes a line that's there. */
|
|
25
|
+
export declare function planEnv(plan: Plan, vars: Record<string, string | undefined>): string[];
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
// src/cli/files.ts
|
|
4
|
+
const ext = (d) => d.typescript ? "ts" : "js";
|
|
5
|
+
const base = (d) => d.srcDir ? "src/" : "";
|
|
6
|
+
/** Import specifier from one project file to another (extension dropped). */
|
|
7
|
+
function relImport(fromFile, toFile) {
|
|
8
|
+
const r = posix.relative(posix.dirname(fromFile), toFile).replace(/\.(tsx?|jsx?|mjs)$/, "");
|
|
9
|
+
return r.startsWith("./") || r.startsWith("../") ? r : `./${r}`;
|
|
10
|
+
}
|
|
11
|
+
const configFile = (d) => `evolve.config.${ext(d)}`;
|
|
12
|
+
const goalsFile = (d) => `evolve.goals.${ext(d)}`;
|
|
13
|
+
const routeFile = (d) => `${d.appDir}/api/evolve/[...evolve]/route.${ext(d)}`;
|
|
14
|
+
const matcherFor = (routes) => [...routes.map((r) => r.path), "/(ev_.*)"];
|
|
15
|
+
const list = (xs) => `[${xs.map((x) => JSON.stringify(x)).join(", ")}]`;
|
|
16
|
+
function planConfig(plan, d, routes) {
|
|
17
|
+
const routeLines = routes.map((r) => ` ${JSON.stringify(r.path)}: { mode: "${r.mode}" },`);
|
|
18
|
+
if (!plan.create(configFile(d), `import { defineConfig } from "next-evolve/config";
|
|
19
|
+
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
// Pages Evolve may test. "static": cached per variant (moved under app/[evolve]/).
|
|
22
|
+
// "dynamic": rendered per request, the page stays where it is.
|
|
23
|
+
routes: {
|
|
24
|
+
${routeLines.join("\n")}
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
`)) {
|
|
28
|
+
const current = plan.read(configFile(d)) ?? "";
|
|
29
|
+
const missing = routes.filter((r) => !current.includes(JSON.stringify(r.path)));
|
|
30
|
+
if (missing.length) plan.manual.push(`Add to routes in ${configFile(d)}: ${missing.map((r) => `${JSON.stringify(r.path)}: { mode: "${r.mode}" }`).join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function planGoals(plan, d) {
|
|
34
|
+
plan.create(goalsFile(d), `import { defineGoals } from "next-evolve/config";
|
|
35
|
+
|
|
36
|
+
// What counts as success. Fire events with data-evolve-goal="<event>" on a
|
|
37
|
+
// link or form, or evolve.track("<event>") on the server.
|
|
38
|
+
export default defineGoals({
|
|
39
|
+
signed_up: { event: "signup", window: "7d" },
|
|
40
|
+
});
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
function planRouteHandler(plan, d) {
|
|
44
|
+
const f = routeFile(d);
|
|
45
|
+
plan.create(f, `import { createEvolveHandler } from "next-evolve/server";
|
|
46
|
+
import evolveConfig from "${relImport(f, configFile(d))}";
|
|
47
|
+
import goals from "${relImport(f, goalsFile(d))}";
|
|
48
|
+
|
|
49
|
+
// collect (beacon), track, sync (cloud webhook), aggregate (cloud), health
|
|
50
|
+
export const { GET, POST } = createEvolveHandler({ config: evolveConfig, goals });
|
|
51
|
+
export const maxDuration = 60;
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
function planProxy(plan, d, routes) {
|
|
55
|
+
const matcher = list(matcherFor(routes));
|
|
56
|
+
if (d.proxyFile) {
|
|
57
|
+
if ((plan.read(d.proxyFile) ?? "").includes("next-evolve")) return;
|
|
58
|
+
plan.manual.push(`Your ${d.proxyFile} exists, so add Evolve to it:
|
|
59
|
+
import { applyEvolve } from "next-evolve/proxy";
|
|
60
|
+
import buildBundle from "${relImport(d.proxyFile, ".evolve/bundle.generated.ts")}";
|
|
61
|
+
import evolveConfig from "${relImport(d.proxyFile, configFile(d))}";
|
|
62
|
+
// first thing in your ${d.proxyKind} function:
|
|
63
|
+
const evolved = applyEvolve(evolveConfig, req, event, { buildBundle });
|
|
64
|
+
if (evolved) return evolved;
|
|
65
|
+
and add ${matcher.slice(1, -1)} to config.matcher${d.proxyKind === "middleware" ? ", with runtime: \"nodejs\"" : ""}.`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const f = `${base(d)}${d.proxyKind}.${ext(d)}`;
|
|
69
|
+
const imports = `import { createEvolveProxy } from "next-evolve/proxy";
|
|
70
|
+
import buildBundle from "${relImport(f, ".evolve/bundle.generated.ts")}";
|
|
71
|
+
import evolveConfig from "${relImport(f, configFile(d))}";
|
|
72
|
+
`;
|
|
73
|
+
plan.write(f, d.proxyKind === "proxy" ? `${imports}
|
|
74
|
+
export const proxy = createEvolveProxy(evolveConfig, { buildBundle });
|
|
75
|
+
|
|
76
|
+
export const config = { matcher: ${matcher} };
|
|
77
|
+
` : `${imports}
|
|
78
|
+
export const middleware = createEvolveProxy(evolveConfig, { buildBundle });
|
|
79
|
+
|
|
80
|
+
export const config = { matcher: ${matcher}, runtime: "nodejs" };
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
/** Imports go after the last import line (or at the top). */
|
|
84
|
+
function addImport(src, line) {
|
|
85
|
+
const lines = src.split("\n");
|
|
86
|
+
let last = -1;
|
|
87
|
+
lines.forEach((l, i) => {
|
|
88
|
+
if (/^import\s/.test(l) || /^\s*} from ["']/.test(l)) last = i;
|
|
89
|
+
});
|
|
90
|
+
if (last === -1) {
|
|
91
|
+
const directive = /^["']use (client|server)["'];?$/.test(lines[0] ?? "") ? 1 : 0;
|
|
92
|
+
lines.splice(directive, 0, line, "");
|
|
93
|
+
} else lines.splice(last + 1, 0, line);
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
function planLayout(plan, d) {
|
|
97
|
+
const f = [
|
|
98
|
+
"tsx",
|
|
99
|
+
"jsx",
|
|
100
|
+
"js",
|
|
101
|
+
"ts"
|
|
102
|
+
].map((e) => `${d.appDir}/layout.${e}`).find((p) => plan.exists(p));
|
|
103
|
+
if (!f) {
|
|
104
|
+
plan.manual.push(`Add <EvolveScript /> (from "next-evolve/client") to your root layout's <body>.`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const src = plan.read(f);
|
|
108
|
+
if (src.includes("EvolveScript")) return;
|
|
109
|
+
const m = /^([ \t]*)<\/body>/m.exec(src);
|
|
110
|
+
if (!m) {
|
|
111
|
+
plan.manual.push(`Add <EvolveScript /> (from "next-evolve/client") inside <body> in ${f}.`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const indent = `${m[1]} `;
|
|
115
|
+
const withTag = src.replace(m[0], `${indent}<EvolveScript />\n${m[0]}`);
|
|
116
|
+
plan.write(f, addImport(withTag, `import { EvolveScript } from "next-evolve/client";`));
|
|
117
|
+
}
|
|
118
|
+
function planInstrumentation(plan, d) {
|
|
119
|
+
const f = `${base(d)}instrumentation.${ext(d)}`;
|
|
120
|
+
const existing = ["ts", "js"].map((e) => `${base(d)}instrumentation.${e}`).find((p) => plan.exists(p));
|
|
121
|
+
const block = ` if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
122
|
+
const [{ registerEvolve }, { default: config }, { default: goals }] = await Promise.all([
|
|
123
|
+
import("next-evolve/instrumentation"),
|
|
124
|
+
import("${relImport(existing ?? f, configFile(d))}"),
|
|
125
|
+
import("${relImport(existing ?? f, goalsFile(d))}"),
|
|
126
|
+
]);
|
|
127
|
+
await registerEvolve({ config, goals });
|
|
128
|
+
}`;
|
|
129
|
+
if (!existing) {
|
|
130
|
+
plan.write(f, `export async function register() {\n${block}\n}\n`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const src = plan.read(existing);
|
|
134
|
+
if (src.includes("registerEvolve")) return;
|
|
135
|
+
const m = /export\s+(async\s+)?function\s+register\s*\([^)]*\)\s*(:\s*[^{]+)?\{\n?/.exec(src);
|
|
136
|
+
if (!m) {
|
|
137
|
+
plan.manual.push(`Add to register() in ${existing}:\n${block}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const head = m[1] ? m[0] : m[0].replace(/function/, "async function");
|
|
141
|
+
plan.write(existing, src.replace(m[0], `${head.endsWith("\n") ? head : `${head}\n`}${block}\n`));
|
|
142
|
+
}
|
|
143
|
+
function planPackageJson(plan) {
|
|
144
|
+
const src = plan.read("package.json");
|
|
145
|
+
if (!src) return;
|
|
146
|
+
const pkg = JSON.parse(src);
|
|
147
|
+
const pre = pkg.scripts?.prebuild;
|
|
148
|
+
if (pre?.includes("next-evolve pull")) return;
|
|
149
|
+
pkg.scripts = {
|
|
150
|
+
...pkg.scripts,
|
|
151
|
+
prebuild: pre ? `next-evolve pull && ${pre}` : "next-evolve pull"
|
|
152
|
+
};
|
|
153
|
+
const indent = /^(\s+)"/m.exec(src)?.[1] ?? " ";
|
|
154
|
+
plan.write("package.json", `${JSON.stringify(pkg, null, indent)}\n`);
|
|
155
|
+
}
|
|
156
|
+
function planGitignore(plan) {
|
|
157
|
+
const src = plan.read(".gitignore") ?? "";
|
|
158
|
+
if (/^\/?\.evolve\/?$/m.test(src)) return;
|
|
159
|
+
plan.write(".gitignore", `${src}${src && !src.endsWith("\n") ? "\n" : ""}\n# Evolve build bundle\n.evolve/\n`);
|
|
160
|
+
}
|
|
161
|
+
const BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
162
|
+
const newTrackSecret = () => `trk_${Array.from(randomBytes(32), (b) => BASE62[b % 62]).join("")}`;
|
|
163
|
+
/** Appends variables missing from .env.local; never changes a line that's there. */
|
|
164
|
+
function planEnv(plan, vars) {
|
|
165
|
+
const src = plan.read(".env.local") ?? "";
|
|
166
|
+
const has = (k) => new RegExp(`^\\s*(export\\s+)?${k}\\s*=`, "m").test(src);
|
|
167
|
+
const add = Object.entries(vars).filter(([k, v]) => v && !has(k));
|
|
168
|
+
if (!add.length) return [];
|
|
169
|
+
const lines = add.map(([k, v]) => `${k}=${v}`).join("\n");
|
|
170
|
+
plan.write(".env.local", `${src}${src && !src.endsWith("\n") ? "\n" : ""}${src ? "\n" : ""}# Evolve\n${lines}\n`);
|
|
171
|
+
return add.map(([k]) => k);
|
|
172
|
+
}
|
|
173
|
+
export { addImport, configFile, goalsFile, matcherFor, newTrackSecret, planConfig, planEnv, planGitignore, planGoals, planInstrumentation, planLayout, planPackageJson, planProxy, planRouteHandler, relImport, routeFile };
|