next-evolve 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +136 -2
- 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.d.ts +1 -0
- package/dist/cli/index.js +81 -0
- 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/client/beacon.d.ts +11 -0
- package/dist/client/beacon.js +51 -0
- package/dist/client/evolve-script.d.ts +6 -0
- package/dist/client/evolve-script.js +60 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/cloud/manifest.d.ts +14 -0
- package/dist/cloud/manifest.js +46 -0
- package/dist/cloud/push.d.ts +21 -0
- package/dist/cloud/push.js +56 -0
- package/dist/cloud/regions.d.ts +8 -0
- package/dist/cloud/regions.js +25 -0
- package/dist/config/config.d.ts +51 -0
- package/dist/config/config.js +47 -0
- package/dist/config/goals.d.ts +50 -0
- package/dist/config/goals.js +85 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +3 -0
- package/dist/core/assign.d.ts +35 -0
- package/dist/core/assign.js +104 -0
- package/dist/core/bots.d.ts +5 -0
- package/dist/core/bots.js +25 -0
- package/dist/core/constants.d.ts +3 -0
- package/dist/core/constants.js +5 -0
- package/dist/core/cookies.d.ts +12 -0
- package/dist/core/cookies.js +25 -0
- package/dist/core/keys.d.ts +14 -0
- package/dist/core/keys.js +38 -0
- package/dist/core/log.d.ts +2 -0
- package/dist/core/log.js +10 -0
- package/dist/db/client.d.ts +22 -0
- package/dist/db/client.js +50 -0
- package/dist/db/migrate.d.ts +18 -0
- package/dist/db/migrate.js +43 -0
- package/dist/db/writer.d.ts +53 -0
- package/dist/db/writer.js +182 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/instrumentation/index.d.ts +24 -0
- package/dist/instrumentation/index.js +54 -0
- 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 -0
- package/dist/proxy/index.js +1 -0
- package/dist/proxy/proxy.d.ts +24 -0
- package/dist/proxy/proxy.js +120 -0
- package/dist/react/context.d.ts +6 -0
- package/dist/react/context.js +8 -0
- package/dist/react/optimizable.d.ts +37 -0
- package/dist/react/optimizable.js +38 -0
- package/dist/react/region.d.ts +17 -0
- package/dist/react/region.js +127 -0
- package/dist/react/render-bundle.d.ts +15 -0
- package/dist/react/render-bundle.js +43 -0
- package/dist/react/with-evolve.d.ts +29 -0
- package/dist/react/with-evolve.js +57 -0
- package/dist/server/aggregate-route.d.ts +16 -0
- package/dist/server/aggregate-route.js +68 -0
- package/dist/server/collect.d.ts +62 -0
- package/dist/server/collect.js +157 -0
- package/dist/server/events.d.ts +18 -0
- package/dist/server/events.js +50 -0
- package/dist/server/evolve.d.ts +20 -0
- package/dist/server/evolve.js +57 -0
- package/dist/server/handler.d.ts +17 -0
- package/dist/server/handler.js +105 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +4 -0
- package/dist/server/runtime.d.ts +26 -0
- package/dist/server/runtime.js +45 -0
- package/dist/server/sync-route.d.ts +28 -0
- package/dist/server/sync-route.js +100 -0
- package/dist/server/track-route.d.ts +25 -0
- package/dist/server/track-route.js +49 -0
- package/dist/store/disk.d.ts +6 -0
- package/dist/store/disk.js +23 -0
- package/dist/store/fetch.d.ts +26 -0
- package/dist/store/fetch.js +74 -0
- package/dist/store/global-config.d.ts +39 -0
- package/dist/store/global-config.js +122 -0
- package/dist/store/memory.d.ts +39 -0
- package/dist/store/memory.js +72 -0
- package/dist/store/runtime.d.ts +44 -0
- package/dist/store/runtime.js +101 -0
- package/migrations/0001_init.sql +56 -0
- package/package.json +73 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type Env = Record<string, string | undefined>;
|
|
2
|
+
export type CloudDeps = {
|
|
3
|
+
env: Env;
|
|
4
|
+
fetch?: typeof fetch;
|
|
5
|
+
};
|
|
6
|
+
export type SiteInfo = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
appUrl: string | null;
|
|
10
|
+
};
|
|
11
|
+
export declare const cloudUrl: (env: Env) => string;
|
|
12
|
+
/** `GET /v1/site`: which site this key belongs to. Accepts the cloud's and the fake cloud's shapes. */
|
|
13
|
+
export declare function getSiteInfo(deps: CloudDeps, key: string): Promise<{
|
|
14
|
+
ok: true;
|
|
15
|
+
site: SiteInfo;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
error: string;
|
|
19
|
+
}>;
|
|
20
|
+
/** `PUT /v1/site/app-url`. */
|
|
21
|
+
export declare function putAppUrl(deps: CloudDeps, key: string, appUrl: string): Promise<{
|
|
22
|
+
ok: true;
|
|
23
|
+
appUrl: string;
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
error: string;
|
|
27
|
+
}>;
|
|
28
|
+
export type CheckResult = {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
detail?: string;
|
|
31
|
+
};
|
|
32
|
+
/** `POST /v1/site/test`: the cloud pings the sync route and dry-runs an aggregate. */
|
|
33
|
+
export declare function runCloudTest(deps: CloudDeps, key: string): Promise<{
|
|
34
|
+
ok: true;
|
|
35
|
+
webhook: CheckResult;
|
|
36
|
+
aggregate: CheckResult;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
error: string;
|
|
40
|
+
}>;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import "../store/runtime.js";
|
|
2
|
+
// src/cli/cloud.ts
|
|
3
|
+
const cloudUrl = (env) => env.EVOLVE_CLOUD_URL ?? "https://api.evolveab.com";
|
|
4
|
+
async function call(deps, key, method, path, body) {
|
|
5
|
+
try {
|
|
6
|
+
const res = await (deps.fetch ?? fetch)(new URL(path, cloudUrl(deps.env)), {
|
|
7
|
+
method,
|
|
8
|
+
headers: {
|
|
9
|
+
authorization: `Bearer ${key}`,
|
|
10
|
+
...body === void 0 ? {} : { "content-type": "application/json" }
|
|
11
|
+
},
|
|
12
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
13
|
+
signal: AbortSignal.timeout(3e4)
|
|
14
|
+
});
|
|
15
|
+
const text = await res.text();
|
|
16
|
+
let json = null;
|
|
17
|
+
try {
|
|
18
|
+
json = JSON.parse(text);
|
|
19
|
+
} catch {}
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const err = json ?? {};
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
status: res.status,
|
|
25
|
+
error: res.status === 401 ? "the secret key was rejected (check EVOLVE_SECRET_KEY)" : `HTTP ${res.status}${err.message ? `: ${err.message}` : err.error ? `: ${err.error}` : ""}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
status: res.status,
|
|
31
|
+
json
|
|
32
|
+
};
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: `can't reach ${cloudUrl(deps.env)} (${e instanceof Error ? e.message : e})`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** `GET /v1/site`: which site this key belongs to. Accepts the cloud's and the fake cloud's shapes. */
|
|
41
|
+
async function getSiteInfo(deps, key) {
|
|
42
|
+
const r = await call(deps, key, "GET", "/v1/site");
|
|
43
|
+
if (!r.ok) return r;
|
|
44
|
+
const j = r.json;
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
site: j.site && typeof j.site === "object" ? j.site : {
|
|
48
|
+
id: String(j.site ?? ""),
|
|
49
|
+
name: j.name ?? "",
|
|
50
|
+
appUrl: j.appUrl ?? null
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** `PUT /v1/site/app-url`. */
|
|
55
|
+
async function putAppUrl(deps, key, appUrl) {
|
|
56
|
+
const r = await call(deps, key, "PUT", "/v1/site/app-url", { appUrl });
|
|
57
|
+
if (!r.ok) return r;
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
appUrl: r.json.appUrl
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** `POST /v1/site/test`: the cloud pings the sync route and dry-runs an aggregate. */
|
|
64
|
+
async function runCloudTest(deps, key) {
|
|
65
|
+
const r = await call(deps, key, "POST", "/v1/site/test");
|
|
66
|
+
if (!r.ok) return r;
|
|
67
|
+
const j = r.json;
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
webhook: j.webhook,
|
|
71
|
+
aggregate: j.aggregate
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export { cloudUrl, getSiteInfo, putAppUrl, runCloudTest };
|
|
@@ -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[];
|