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
package/dist/cli/index.js
CHANGED
|
@@ -1,40 +1,81 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { aggregateCommand } from "./aggregate.js";
|
|
3
|
+
import { connectCommand } from "./connect.js";
|
|
4
|
+
import { doctorCommand } from "./doctor.js";
|
|
5
|
+
import { initCommand } from "./init.js";
|
|
6
|
+
import { migrateCommand } from "./migrate.js";
|
|
7
|
+
import { pullCommand } from "./pull.js";
|
|
8
|
+
import { pushCommand } from "./push.js";
|
|
4
9
|
import { existsSync } from "node:fs";
|
|
5
10
|
// src/cli/index.ts
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const COMMANDS = {
|
|
12
|
+
init: {
|
|
13
|
+
usage: "init [--key] [--webhook-secret] [--db] [--app-url] [--mode] [--routes] [--yes] [--dry-run]",
|
|
14
|
+
summary: "Set Evolve up in this Next.js app (safe to re-run)",
|
|
15
|
+
run: async (args, env) => {
|
|
16
|
+
if (!(process.stdin.isTTY && !args.includes("--yes") && !args.includes("-y"))) return initCommand(args, {
|
|
17
|
+
env,
|
|
18
|
+
ask: null
|
|
19
|
+
});
|
|
20
|
+
const { createInterface } = await import("node:readline/promises");
|
|
21
|
+
const rl = createInterface({
|
|
22
|
+
input: process.stdin,
|
|
23
|
+
output: process.stdout
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
return await initCommand(args, {
|
|
27
|
+
env,
|
|
28
|
+
ask: (q) => rl.question(q)
|
|
29
|
+
});
|
|
30
|
+
} finally {
|
|
31
|
+
rl.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
migrate: {
|
|
36
|
+
usage: "migrate [--url <postgres-url>]",
|
|
37
|
+
summary: "Create or update the evolve schema in your database ($EVOLVE_DATABASE_URL)",
|
|
38
|
+
run: (args, env) => migrateCommand(args, { env })
|
|
39
|
+
},
|
|
40
|
+
pull: {
|
|
41
|
+
usage: "pull",
|
|
42
|
+
summary: "Write the verified bundle to .evolve/bundle.generated.ts (run as \"prebuild\")",
|
|
43
|
+
run: (args, env) => pullCommand(args, { env })
|
|
44
|
+
},
|
|
45
|
+
push: {
|
|
46
|
+
usage: "push",
|
|
47
|
+
summary: "Send routes and goals (the manifest) to Evolve Cloud now",
|
|
48
|
+
run: (args, env) => pushCommand(args, { env })
|
|
49
|
+
},
|
|
50
|
+
connect: {
|
|
51
|
+
usage: "connect [--test] [--app-url <url>]",
|
|
52
|
+
summary: "Check the key; register the app URL; --test: can the cloud reach your app?",
|
|
53
|
+
run: (args, env) => connectCommand(args, { env })
|
|
54
|
+
},
|
|
55
|
+
doctor: {
|
|
56
|
+
usage: "doctor",
|
|
57
|
+
summary: "Check the whole setup (env, key, DB, bundle, proxy, pages, cloud → app)",
|
|
58
|
+
run: (args, env) => doctorCommand(args, { env })
|
|
59
|
+
},
|
|
60
|
+
aggregate: {
|
|
61
|
+
usage: "aggregate [--upload]",
|
|
62
|
+
summary: "Run the cloud's aggregate request on your local DB (--upload sends the result)",
|
|
63
|
+
run: (args, env) => aggregateCommand(args, { env })
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
function help() {
|
|
67
|
+
const width = Math.max(...Object.values(COMMANDS).map((c) => c.usage.length)) + 2;
|
|
68
|
+
return `Usage: next-evolve <command>\n\nCommands:\n${Object.values(COMMANDS).map((c) => ` ${c.usage.padEnd(width)}${c.summary}`).join("\n")}\n\nEnv is read from .env.local and .env (never overriding what's set).\n`;
|
|
69
|
+
}
|
|
12
70
|
function loadEnvFiles() {
|
|
13
71
|
for (const f of [".env.local", ".env"]) if (existsSync(f)) process.loadEnvFile(f);
|
|
14
72
|
}
|
|
15
|
-
async function runMigrate(args) {
|
|
16
|
-
loadEnvFiles();
|
|
17
|
-
const i = args.indexOf("--url");
|
|
18
|
-
const url = i >= 0 ? args[i + 1] : process.env.EVOLVE_DATABASE_URL;
|
|
19
|
-
if (!url) {
|
|
20
|
-
console.error("next-evolve migrate: set EVOLVE_DATABASE_URL or pass --url");
|
|
21
|
-
return 1;
|
|
22
|
-
}
|
|
23
|
-
const sql = createSql(url, { max: 1 });
|
|
24
|
-
try {
|
|
25
|
-
const r = await migrate(sql);
|
|
26
|
-
console.warn(r.applied.length ? `Applied ${r.applied.join(", ")}` : `Up to date (${r.skipped.join(", ") || "nothing to apply"})`);
|
|
27
|
-
return 0;
|
|
28
|
-
} catch (e) {
|
|
29
|
-
console.error("next-evolve migrate failed:", e instanceof Error ? e.message : e);
|
|
30
|
-
return 1;
|
|
31
|
-
} finally {
|
|
32
|
-
await sql.end({ timeout: 5 });
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
73
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
process.exitCode =
|
|
74
|
+
const command = cmd ? COMMANDS[cmd] : void 0;
|
|
75
|
+
if (command) {
|
|
76
|
+
loadEnvFiles();
|
|
77
|
+
process.exitCode = await command.run(rest, process.env);
|
|
78
|
+
} else {
|
|
79
|
+
process.stdout.write(help());
|
|
80
|
+
process.exitCode = cmd && cmd !== "help" && cmd !== "--help" && cmd !== "-h" ? 1 : 0;
|
|
40
81
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Detection } from "./detect.js";
|
|
2
|
+
type Env = Record<string, string | undefined>;
|
|
3
|
+
export type InitDeps = {
|
|
4
|
+
env: Env;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
out?: (s: string) => void;
|
|
8
|
+
err?: (s: string) => void;
|
|
9
|
+
/** Asks a question (interactive); null when there's no terminal. */
|
|
10
|
+
ask?: ((q: string) => Promise<string>) | null;
|
|
11
|
+
/** Runs a command (install). */
|
|
12
|
+
run?: (cmd: string, args: string[], cwd: string) => Promise<number>;
|
|
13
|
+
/** Tests the DB and migrates it. */
|
|
14
|
+
setupDb?: (url: string) => Promise<{
|
|
15
|
+
ok: true;
|
|
16
|
+
applied: string[];
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
error: string;
|
|
20
|
+
}>;
|
|
21
|
+
/** Runs `push` (tests can skip). */
|
|
22
|
+
push?: (env: Env) => Promise<number>;
|
|
23
|
+
};
|
|
24
|
+
export declare function initCommand(args: string[], deps: InitDeps): Promise<number>;
|
|
25
|
+
export declare function installCommand(d: Detection, pkgs: string[]): string[];
|
|
26
|
+
export {};
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { createSql } from "../db/client.js";
|
|
2
|
+
import { migrate } from "../db/migrate.js";
|
|
3
|
+
import { parseFlags } from "./args.js";
|
|
4
|
+
import { getSiteInfo, putAppUrl } from "./cloud.js";
|
|
5
|
+
import { detectProject } from "./detect.js";
|
|
6
|
+
import { fileEnv } from "./env-file.js";
|
|
7
|
+
import { newTrackSecret, planConfig, planEnv, planGitignore, planGoals, planInstrumentation, planLayout, planPackageJson, planProxy, planRouteHandler } from "./files.js";
|
|
8
|
+
import { planPages } from "./pages.js";
|
|
9
|
+
import { Plan, renderDiff } from "./plan.js";
|
|
10
|
+
import { pullCommand } from "./pull.js";
|
|
11
|
+
import { pushCommand } from "./push.js";
|
|
12
|
+
import { WEBHOOK_SECRET_RE, parseSecretKey } from "@next-evolve/protocol";
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
// src/cli/init.ts
|
|
15
|
+
const FLAGS = {
|
|
16
|
+
key: { type: "string" },
|
|
17
|
+
"webhook-secret": { type: "string" },
|
|
18
|
+
db: { type: "string" },
|
|
19
|
+
"app-url": { type: "string" },
|
|
20
|
+
yes: {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
short: "y"
|
|
23
|
+
},
|
|
24
|
+
"dry-run": { type: "boolean" },
|
|
25
|
+
mode: { type: "string" },
|
|
26
|
+
routes: { type: "string" }
|
|
27
|
+
};
|
|
28
|
+
const DB_HELP = `Evolve stores events in your own Postgres (its own "evolve" schema).
|
|
29
|
+
One-click options:
|
|
30
|
+
Vercel Marketplace → Neon: https://vercel.com/marketplace/neon
|
|
31
|
+
Supabase: https://supabase.com/dashboard/new
|
|
32
|
+
Neon: https://console.neon.tech/signup`;
|
|
33
|
+
async function initCommand(args, deps) {
|
|
34
|
+
const out = deps.out ?? ((s) => console.warn(s));
|
|
35
|
+
const err = deps.err ?? ((s) => console.error(s));
|
|
36
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
37
|
+
const p = parseFlags(args, FLAGS);
|
|
38
|
+
if (!p.ok) {
|
|
39
|
+
err(`next-evolve init: ${p.error}`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
const f = p.flags;
|
|
43
|
+
const dry = Boolean(f["dry-run"]);
|
|
44
|
+
const yes = Boolean(f.yes);
|
|
45
|
+
const ask = deps.ask === void 0 ? null : deps.ask;
|
|
46
|
+
const env = {
|
|
47
|
+
...fileEnv(cwd),
|
|
48
|
+
...deps.env
|
|
49
|
+
};
|
|
50
|
+
const d = detectProject(cwd);
|
|
51
|
+
for (const pr of d.problems) (pr.level === "error" ? err : out)(`${pr.level === "error" ? "✗" : "!"} ${pr.message}`);
|
|
52
|
+
if (d.problems.some((pr) => pr.level === "error")) return 1;
|
|
53
|
+
out(`✓ Next.js ${d.nextVersion} · ${d.appDir}/ · ${d.typescript ? "TypeScript" : "JavaScript"} · ${d.packageManager}`);
|
|
54
|
+
const mode = f.mode ?? "static";
|
|
55
|
+
if (mode !== "static" && mode !== "dynamic") {
|
|
56
|
+
err("next-evolve init: --mode is \"static\" or \"dynamic\"");
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
const paths = (f.routes ?? "/").split(/[\s,]+/).filter(Boolean);
|
|
60
|
+
if (paths.some((r) => !r.startsWith("/") || /[[\]*?#]/.test(r))) {
|
|
61
|
+
err("next-evolve init: --routes are concrete paths like \"/ /pricing\"");
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
const need = async (label, flag, envKey, valid) => {
|
|
65
|
+
let v = flag ?? env[envKey];
|
|
66
|
+
if (!v && ask && !yes) v = (await ask(`${label}: `)).trim();
|
|
67
|
+
if (!v || !valid(v)) return null;
|
|
68
|
+
return v;
|
|
69
|
+
};
|
|
70
|
+
const key = await need("Secret key (evk_live_…)", f.key, "EVOLVE_SECRET_KEY", (v) => Boolean(parseSecretKey(v)));
|
|
71
|
+
if (!key) {
|
|
72
|
+
err("✗ A secret key is needed: --key evk_live_… (shown once when you created the site)");
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
const webhookSecret = await need("Webhook secret (whsec_…)", f["webhook-secret"], "EVOLVE_WEBHOOK_SECRET", (v) => WEBHOOK_SECRET_RE.test(v));
|
|
76
|
+
if (!webhookSecret) {
|
|
77
|
+
err("✗ The webhook secret is needed: --webhook-secret whsec_…");
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
const site = await getSiteInfo({
|
|
81
|
+
env,
|
|
82
|
+
fetch: deps.fetch
|
|
83
|
+
}, key);
|
|
84
|
+
if (!site.ok) {
|
|
85
|
+
err(`✗ ${site.error}`);
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
out(`✓ Connected to "${site.site.name}" (${site.site.id})`);
|
|
89
|
+
const missing = [...d.hasEvolve ? [] : ["next-evolve"], ...d.hasZod ? [] : ["zod@^4"]];
|
|
90
|
+
if (missing.length) {
|
|
91
|
+
if (dry) out(` would install ${missing.join(" ")}`);
|
|
92
|
+
else {
|
|
93
|
+
const [cmd, ...a] = installCommand(d, missing);
|
|
94
|
+
out(` ${cmd} ${a.join(" ")}`);
|
|
95
|
+
const code = await (deps.run ?? runCommand)(cmd, a, cwd);
|
|
96
|
+
if (code !== 0) {
|
|
97
|
+
err(`✗ Install failed (exit ${code})`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
let dbUrl = f.db ?? env.EVOLVE_DATABASE_URL ?? null;
|
|
103
|
+
if (!dbUrl) {
|
|
104
|
+
const reuse = env.DATABASE_URL ? "DATABASE_URL" : env.POSTGRES_URL ? "POSTGRES_URL" : null;
|
|
105
|
+
if (reuse) {
|
|
106
|
+
if (yes || !ask || /^(y|yes|)$/i.test((await ask(`Use ${reuse} for Evolve's events (own "evolve" schema)? [Y/n] `)).trim())) dbUrl = env[reuse];
|
|
107
|
+
}
|
|
108
|
+
if (!dbUrl && ask && !yes) {
|
|
109
|
+
out(DB_HELP);
|
|
110
|
+
dbUrl = (await ask("Postgres URL (empty to skip for now): ")).trim() || null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!dbUrl) out(`! No database yet: variants serve, but nothing is measured.\n${DB_HELP}\n Then: set EVOLVE_DATABASE_URL and run npx next-evolve migrate`);
|
|
114
|
+
else if (dry) out(" would test the database and run migrations");
|
|
115
|
+
else {
|
|
116
|
+
const r = await (deps.setupDb ?? setupDatabase)(dbUrl);
|
|
117
|
+
if (!r.ok) {
|
|
118
|
+
err(`✗ Database: ${r.error}`);
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
out(`✓ Database ready${r.applied.length ? ` (applied ${r.applied.join(", ")})` : ""}`);
|
|
122
|
+
}
|
|
123
|
+
const plan = new Plan(cwd);
|
|
124
|
+
const routes = paths.map((path) => ({
|
|
125
|
+
path,
|
|
126
|
+
mode
|
|
127
|
+
}));
|
|
128
|
+
const finalRoutes = planPages(plan, d, routes);
|
|
129
|
+
planConfig(plan, d, finalRoutes);
|
|
130
|
+
planGoals(plan, d);
|
|
131
|
+
planRouteHandler(plan, d);
|
|
132
|
+
planProxy(plan, d, finalRoutes);
|
|
133
|
+
planLayout(plan, d);
|
|
134
|
+
planInstrumentation(plan, d);
|
|
135
|
+
planPackageJson(plan);
|
|
136
|
+
planGitignore(plan);
|
|
137
|
+
const added = planEnv(plan, {
|
|
138
|
+
EVOLVE_SECRET_KEY: key,
|
|
139
|
+
EVOLVE_WEBHOOK_SECRET: webhookSecret,
|
|
140
|
+
EVOLVE_DATABASE_URL: dbUrl ?? void 0,
|
|
141
|
+
EVOLVE_TRACK_SECRET: env.EVOLVE_TRACK_SECRET ?? newTrackSecret()
|
|
142
|
+
});
|
|
143
|
+
const changes = plan.changes();
|
|
144
|
+
if (dry) out(changes.length ? `\nWould change:\n\n${renderDiff(changes)}` : "\nNothing to change.");
|
|
145
|
+
else {
|
|
146
|
+
plan.apply();
|
|
147
|
+
for (const c of changes) out(` ${c.before === null ? "created" : c.after === null ? "removed" : "updated"} ${c.path}`);
|
|
148
|
+
if (!changes.length) out(" files already set up");
|
|
149
|
+
}
|
|
150
|
+
for (const n of plan.notes) out(` ${n}`);
|
|
151
|
+
const appEnv = {
|
|
152
|
+
...env,
|
|
153
|
+
EVOLVE_SECRET_KEY: key,
|
|
154
|
+
EVOLVE_WEBHOOK_SECRET: webhookSecret
|
|
155
|
+
};
|
|
156
|
+
if (!dry) {
|
|
157
|
+
await pullCommand([], {
|
|
158
|
+
env: appEnv,
|
|
159
|
+
cwd,
|
|
160
|
+
out: () => {},
|
|
161
|
+
err: () => {}
|
|
162
|
+
});
|
|
163
|
+
if (f["app-url"]) {
|
|
164
|
+
const r = await putAppUrl({
|
|
165
|
+
env,
|
|
166
|
+
fetch: deps.fetch
|
|
167
|
+
}, key, f["app-url"]);
|
|
168
|
+
if (r.ok) out(`✓ App URL: ${r.appUrl}`);
|
|
169
|
+
else err(`! App URL not set: ${r.error}`);
|
|
170
|
+
}
|
|
171
|
+
out(await (deps.push ?? ((e) => pushCommand([], {
|
|
172
|
+
env: e,
|
|
173
|
+
cwd,
|
|
174
|
+
out: () => {},
|
|
175
|
+
err: () => {},
|
|
176
|
+
quiet: true
|
|
177
|
+
})))(appEnv) === 0 ? "✓ Manifest sent" : "! Manifest not sent yet (it's sent when your app starts)");
|
|
178
|
+
} else if (f["app-url"]) out(` would register the app URL ${f["app-url"]}`);
|
|
179
|
+
out(`\nNext:\n${[
|
|
180
|
+
...plan.manual,
|
|
181
|
+
...added.length && !dry ? [`In production, set ${added.join(", ")} (they're in .env.local now)`] : [],
|
|
182
|
+
`Wrap a component: <Optimizable id="hero" component={Hero} props={...} /> (from "next-evolve")`,
|
|
183
|
+
"Visit the page once so the region registers, then open the dashboard",
|
|
184
|
+
...f["app-url"] ? ["After you deploy: npx next-evolve connect --test"] : ["After you deploy: npx next-evolve connect --test --app-url https://your-app.com"]
|
|
185
|
+
].map((s, i) => ` ${i + 1}. ${s}`).join("\n")}`);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
function installCommand(d, pkgs) {
|
|
189
|
+
switch (d.packageManager) {
|
|
190
|
+
case "pnpm": return [
|
|
191
|
+
"pnpm",
|
|
192
|
+
"add",
|
|
193
|
+
...pkgs
|
|
194
|
+
];
|
|
195
|
+
case "yarn": return [
|
|
196
|
+
"yarn",
|
|
197
|
+
"add",
|
|
198
|
+
...pkgs
|
|
199
|
+
];
|
|
200
|
+
case "bun": return [
|
|
201
|
+
"bun",
|
|
202
|
+
"add",
|
|
203
|
+
...pkgs
|
|
204
|
+
];
|
|
205
|
+
default: return [
|
|
206
|
+
"npm",
|
|
207
|
+
"install",
|
|
208
|
+
...pkgs
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function runCommand(cmd, args, cwd) {
|
|
213
|
+
return new Promise((resolve) => {
|
|
214
|
+
const child = spawn(cmd, args, {
|
|
215
|
+
cwd,
|
|
216
|
+
stdio: "inherit",
|
|
217
|
+
shell: process.platform === "win32"
|
|
218
|
+
});
|
|
219
|
+
child.on("exit", (code) => resolve(code ?? 1));
|
|
220
|
+
child.on("error", () => resolve(1));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
async function setupDatabase(url) {
|
|
224
|
+
const sql = createSql(url, { max: 1 });
|
|
225
|
+
try {
|
|
226
|
+
await sql`select 1`;
|
|
227
|
+
return {
|
|
228
|
+
ok: true,
|
|
229
|
+
applied: (await migrate(sql)).applied
|
|
230
|
+
};
|
|
231
|
+
} catch (e) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
error: e instanceof Error ? e.message : String(e)
|
|
235
|
+
};
|
|
236
|
+
} finally {
|
|
237
|
+
await sql.end({ timeout: 5 }).catch(() => {});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export { initCommand, installCommand };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EvolveConfig } from "../config/config.js";
|
|
2
|
+
import { Goals } from "../config/goals.js";
|
|
3
|
+
export type AppModules = {
|
|
4
|
+
config: EvolveConfig;
|
|
5
|
+
goals?: Goals;
|
|
6
|
+
};
|
|
7
|
+
/** True when this Node can import .ts files (type stripping on). */
|
|
8
|
+
export declare const canImportTs: () => boolean;
|
|
9
|
+
export declare function loadAppModules(root: string): Promise<{
|
|
10
|
+
ok: true;
|
|
11
|
+
mods: AppModules;
|
|
12
|
+
files: string[];
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
error: string;
|
|
16
|
+
needsTs?: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
/** Re-runs this CLI command with type stripping on (Node < 22.18 has it behind a flag). */
|
|
19
|
+
export declare function reexecWithTypes(command: string, args: string[], cwd: string, env: Record<string, string | undefined>, quiet?: boolean): number;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
// src/cli/load-app.ts
|
|
6
|
+
const find = (root, name) => [
|
|
7
|
+
"ts",
|
|
8
|
+
"mts",
|
|
9
|
+
"js",
|
|
10
|
+
"mjs"
|
|
11
|
+
].map((e) => join(root, `${name}.${e}`)).find((p) => existsSync(p));
|
|
12
|
+
/** True when this Node can import .ts files (type stripping on). */
|
|
13
|
+
const canImportTs = () => Boolean(process.features.typescript) || process.execArgv.some((a) => a.includes("strip-types"));
|
|
14
|
+
async function loadAppModules(root) {
|
|
15
|
+
const configPath = find(root, "evolve.config");
|
|
16
|
+
if (!configPath) return {
|
|
17
|
+
ok: false,
|
|
18
|
+
error: "no evolve.config.ts (run npx next-evolve init)"
|
|
19
|
+
};
|
|
20
|
+
const goalsPath = find(root, "evolve.goals");
|
|
21
|
+
const files = [configPath, ...goalsPath ? [goalsPath] : []];
|
|
22
|
+
if (files.some((f) => /\.m?ts$/.test(f)) && !canImportTs()) return {
|
|
23
|
+
ok: false,
|
|
24
|
+
error: "this Node can't load .ts files",
|
|
25
|
+
needsTs: true
|
|
26
|
+
};
|
|
27
|
+
try {
|
|
28
|
+
const c = await import(pathToFileURL(configPath).href);
|
|
29
|
+
const g = goalsPath ? await import(pathToFileURL(goalsPath).href) : void 0;
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
mods: {
|
|
33
|
+
config: c.default ?? c,
|
|
34
|
+
goals: g ? g.default ?? g : void 0
|
|
35
|
+
},
|
|
36
|
+
files
|
|
37
|
+
};
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: `couldn't load ${configPath} (${e instanceof Error ? e.message : e})`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Re-runs this CLI command with type stripping on (Node < 22.18 has it behind a flag). */
|
|
46
|
+
function reexecWithTypes(command, args, cwd, env, quiet = false) {
|
|
47
|
+
return spawnSync(process.execPath, [
|
|
48
|
+
"--experimental-strip-types",
|
|
49
|
+
"--disable-warning=ExperimentalWarning",
|
|
50
|
+
"--disable-warning=MODULE_TYPELESS_PACKAGE_JSON",
|
|
51
|
+
process.argv[1],
|
|
52
|
+
command,
|
|
53
|
+
...args
|
|
54
|
+
], {
|
|
55
|
+
stdio: [
|
|
56
|
+
"ignore",
|
|
57
|
+
quiet ? "ignore" : "inherit",
|
|
58
|
+
"inherit"
|
|
59
|
+
],
|
|
60
|
+
cwd,
|
|
61
|
+
env: {
|
|
62
|
+
...process.env,
|
|
63
|
+
...env
|
|
64
|
+
}
|
|
65
|
+
}).status ?? 1;
|
|
66
|
+
}
|
|
67
|
+
export { canImportTs, loadAppModules, reexecWithTypes };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createSql } from "../db/client.js";
|
|
2
|
+
import { migrate } from "../db/migrate.js";
|
|
3
|
+
import { parseFlags } from "./args.js";
|
|
4
|
+
// src/cli/migrate.ts
|
|
5
|
+
async function migrateCommand(args, deps) {
|
|
6
|
+
const out = deps.out ?? ((s) => console.warn(s));
|
|
7
|
+
const err = deps.err ?? ((s) => console.error(s));
|
|
8
|
+
const parsed = parseFlags(args, { url: { type: "string" } });
|
|
9
|
+
if (!parsed.ok) {
|
|
10
|
+
err(`next-evolve migrate: ${parsed.error}`);
|
|
11
|
+
return 1;
|
|
12
|
+
}
|
|
13
|
+
const url = parsed.flags.url ?? deps.env.EVOLVE_DATABASE_URL;
|
|
14
|
+
if (!url) {
|
|
15
|
+
err("next-evolve migrate: set EVOLVE_DATABASE_URL or pass --url");
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
const sql = createSql(url, { max: 1 });
|
|
19
|
+
try {
|
|
20
|
+
const r = await migrate(sql);
|
|
21
|
+
out(r.applied.length ? `Applied ${r.applied.join(", ")}` : `Up to date (${r.skipped.join(", ") || "nothing to apply"})`);
|
|
22
|
+
return 0;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
err(`next-evolve migrate failed: ${e instanceof Error ? e.message : e}`);
|
|
25
|
+
return 1;
|
|
26
|
+
} finally {
|
|
27
|
+
await sql.end({ timeout: 5 });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export { migrateCommand };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { typescript_d_exports } from "../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/typescript.js";
|
|
2
|
+
import { Detection } from "./detect.js";
|
|
3
|
+
import { RouteMode } from "./files.js";
|
|
4
|
+
import { Plan } from "./plan.js";
|
|
5
|
+
/** The app's typescript, else the one next to next-evolve; null when neither exists. */
|
|
6
|
+
export declare function loadTypescript(root: string): typeof typescript_d_exports | null;
|
|
7
|
+
export declare function planPages(plan: Plan, d: Detection, routes: RouteMode[]): RouteMode[];
|
|
8
|
+
/** Wraps the page module: default export → withEvolve, metadata → evolveMetadata. */
|
|
9
|
+
export declare function wrapPage(ts: typeof typescript_d_exports, src: string, file: string, path: string): {
|
|
10
|
+
ok: true;
|
|
11
|
+
code: string;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
why: string;
|
|
15
|
+
};
|
|
16
|
+
/** Rewrites relative specifiers for a file that moved; files moving together keep pointing at each other. */
|
|
17
|
+
export declare function rewriteImports(ts: typeof typescript_d_exports, src: string, oldPath: string, newPath: string, moves: Map<string, string>): string;
|