@farm.js/cli 0.1.0-beta.7 → 0.1.0-beta.70

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/dist/index.mjs CHANGED
@@ -1,15 +1,17 @@
1
- import { n as listFarmIntegrationProviders, t as addFarmIntegration } from "./add-integration-CdVfiPjG.mjs";
1
+ import { n as listFarmIntegrationProviders, t as addFarmIntegration } from "./add-integration-7hf9WI7e.mjs";
2
2
  import { buildFarm } from "./build.mjs";
3
+ import { a as resolveFarmTelemetryCommand, c as trackFarmCommand, i as resolveFarmCreateAppTelemetryCommand, l as trackFarmCreateAppCommand, n as getFarmTelemetryConfigFile, o as setFarmTelemetryEnabled, r as getFarmTelemetryStatus, s as showFarmTelemetryNotice, t as flushFarmTelemetry, u as trackFarmProjectCreated } from "./telemetry-DjbMk2du.mjs";
3
4
  import { createRequire } from "node:module";
4
5
  import { createServer, startDevServer } from "@farm.js/core/server";
5
- import { existsSync, readFileSync, readdirSync } from "node:fs";
6
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
6
7
  import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
7
8
  import path from "node:path";
8
- import { execFileSync, execSync } from "child_process";
9
+ import { execFileSync, spawn } from "child_process";
9
10
  import { existsSync as existsSync$1, readFileSync as readFileSync$1 } from "fs";
10
11
  import path$1 from "path";
11
- import { generateFarmTypeArtifacts, getFarmDocsRouteTypeEntries, getFarmSourceRoots, getIntegrationSchemas, getPresetForDeployTarget, loadConfig, logger, normalizeDeployTarget, resolveConfig, resolveDeployConfig, resolveDeployOutputPath } from "@farm.js/core";
12
- import { spawn, spawnSync } from "node:child_process";
12
+ import { farmRouteRuleMatches, generateFarmTypeArtifacts, getDeployTargetForPreset, getFarmDocsRouteTypeEntries, getFarmPresetRuntime, getFarmSourceRoots, getIntegrationSchemas, getPresetForDeployTarget, loadConfig, logger, mergeFarmRouteRuntimeConfigs, normalizeDeployTarget, resolveConfig, resolveDeployConfig, resolveDeployOutputPath, resolveFarmRouteRuleRuntimeConfig, resolveFarmRouteRuntimeConfig, resolveRouteRenderingConfig, scanProgrammaticPagePaths } from "@farm.js/core";
13
+ import { constants } from "os";
14
+ import { spawn as spawn$1, spawnSync } from "node:child_process";
13
15
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
14
16
  import pc from "picocolors";
15
17
  import { Cron } from "croner";
@@ -18,36 +20,154 @@ import { pathToFileURL } from "node:url";
18
20
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
19
21
  //#endregion
20
22
  //#region src/deploy.ts
23
+ var FarmDeployError = class extends Error {
24
+ constructor(code, platform, message, options) {
25
+ super(message);
26
+ this.name = "FarmDeployError";
27
+ this.code = code;
28
+ this.platform = platform;
29
+ this.cause = options?.cause;
30
+ }
31
+ };
21
32
  /**
22
33
  * Deploy Farm.js application
23
34
  */
24
35
  async function deployFarm(options = {}) {
25
- const root = options.root || process.cwd();
36
+ const { plan, deployConfig } = await resolveFarmDeployContext(options);
37
+ if (options.plan) {
38
+ logger.info(formatFarmDeployPlan(plan));
39
+ return plan;
40
+ }
41
+ logger.info(`🚀 Building with ${plan.preset} preset...`);
42
+ await buildFarm({
43
+ root: plan.root,
44
+ preset: plan.preset,
45
+ target: plan.target
46
+ });
47
+ if (!existsSync$1(plan.outputDir)) throw new FarmDeployError("INVALID_BUILD_OUTPUT", plan.target, `Build output not found at ${plan.outputDir}. Please run 'farm build' first.`);
48
+ await deployPlatform(plan.target, plan.root, plan.outputDir, deployConfig, options.prod);
49
+ return plan;
50
+ }
51
+ async function createFarmDeployPlan(options = {}) {
52
+ return (await resolveFarmDeployContext(options)).plan;
53
+ }
54
+ function formatFarmDeployPlan(plan) {
55
+ return [
56
+ "FARM / DEPLOY PLAN",
57
+ "",
58
+ `Target: ${plan.target}`,
59
+ `Preset: ${plan.preset}`,
60
+ `Runtime: ${plan.runtime}`,
61
+ `Output: ${plan.outputDir}`,
62
+ `Production: ${plan.production ? "yes" : "no"}`,
63
+ "",
64
+ `1. ${plan.build.command}`,
65
+ ` cwd: ${plan.build.cwd}`,
66
+ `2. ${plan.deploy.command}`,
67
+ ` cwd: ${plan.deploy.cwd}`,
68
+ ...plan.cloudflareAgent ? ["", `Cloudflare Agent config: ${plan.cloudflareAgent.configPath}${plan.cloudflareAgent.generated ? " (generated during build)" : ""}`] : []
69
+ ].join("\n");
70
+ }
71
+ async function resolveFarmDeployContext(options) {
72
+ const root = path$1.resolve(options.root || process.cwd());
26
73
  const mode = "production";
27
74
  const userConfig = await loadConfig(root, void 0, mode);
28
- const config = userConfig ? await resolveConfig(userConfig, mode) : void 0;
75
+ const config = await resolveConfig({
76
+ root,
77
+ ...userConfig
78
+ }, mode);
29
79
  const cliTarget = options.vercel ? "vercel" : options.cloudflare ? "cloudflare" : options.netlify ? "netlify" : void 0;
30
- const platform = normalizeDeployTarget(cliTarget || config?.deploy.target);
31
- if (platform !== "vercel" && platform !== "cloudflare" && platform !== "netlify") {
32
- logger.error("Please specify a deployment target with --vercel, --cloudflare, --netlify, or farm.config deploy.target.");
33
- process.exit(1);
34
- }
80
+ const platform = normalizeDeployTarget(cliTarget || config.deploy.target);
81
+ if (platform !== "vercel" && platform !== "cloudflare" && platform !== "netlify") throw new Error("Please specify a deployment target with --vercel, --cloudflare, --netlify, or farm.config deploy.target.");
82
+ const configuredPreset = userConfig?.deploy?.preset || userConfig?.preset;
83
+ const configuredPresetTarget = getDeployTargetForPreset(configuredPreset);
84
+ const configuredTarget = normalizeDeployTarget(userConfig?.deploy?.target);
85
+ const presetMatchesPlatform = Boolean(configuredPreset) && (configuredPresetTarget === platform || !configuredPresetTarget && configuredTarget === platform);
86
+ if (cliTarget && configuredPreset && !presetMatchesPlatform) logger.warn(`Configured preset "${configuredPreset}" targets ${configuredPresetTarget || "an unknown platform"}; using the "${getPresetForDeployTarget(platform)}" preset because --${cliTarget} was passed.`);
35
87
  const deployConfig = resolveDeployConfig(userConfig || {}, {
36
88
  target: platform,
37
- preset: cliTarget ? userConfig?.deploy?.preset || userConfig?.preset || getPresetForDeployTarget(platform) : void 0
89
+ preset: cliTarget ? presetMatchesPlatform ? configuredPreset : getPresetForDeployTarget(platform) : void 0
38
90
  });
39
91
  const preset = deployConfig.preset || getPresetForDeployTarget(platform) || "node-server";
40
- logger.info(`🚀 Building with ${preset} preset...`);
41
- await buildFarm({
42
- root,
43
- preset
44
- });
45
92
  const nitroOutput = resolveDeployOutputPath(root, deployConfig.outputDir);
46
- if (!existsSync$1(nitroOutput)) {
47
- logger.error(`Build output not found at ${nitroOutput}. Please run 'farm build' first.`);
48
- process.exit(1);
93
+ const cloudflareAgent = platform === "cloudflare" ? resolveCloudflareAgentDeployPlan(root) || resolveConfiguredCloudflareAgentDeployPlan(root, config.integrations) : void 0;
94
+ const deploy = createDeployCommand(platform, root, nitroOutput, deployConfig, options.prod, cloudflareAgent);
95
+ return {
96
+ plan: {
97
+ root: path$1.resolve(root),
98
+ target: platform,
99
+ preset,
100
+ runtime: getFarmPresetRuntime(preset),
101
+ outputDir: nitroOutput,
102
+ production: platform === "netlify" || Boolean(options.prod),
103
+ build: {
104
+ command: `farm build --preset ${preset}`,
105
+ cwd: path$1.resolve(root)
106
+ },
107
+ deploy,
108
+ ...cloudflareAgent ? { cloudflareAgent } : {}
109
+ },
110
+ deployConfig
111
+ };
112
+ }
113
+ function createDeployCommand(platform, root, outputDir, deployConfig, prod, cloudflareAgent) {
114
+ if (platform === "vercel") {
115
+ const args = [
116
+ "deploy",
117
+ "--prebuilt",
118
+ "--yes",
119
+ ...prod ? ["--prod"] : []
120
+ ];
121
+ return {
122
+ command: formatCommand$1("vercel", args),
123
+ cwd: path$1.resolve(root),
124
+ executable: "vercel",
125
+ args
126
+ };
49
127
  }
50
- await deployPlatform(platform, root, nitroOutput, deployConfig, options.prod);
128
+ if (platform === "netlify") {
129
+ const args = createNetlifyDeployArgs(deployConfig.netlify?.site);
130
+ return {
131
+ command: formatCommand$1("netlify", args),
132
+ cwd: outputDir,
133
+ executable: "netlify",
134
+ args
135
+ };
136
+ }
137
+ if (cloudflareAgent) {
138
+ const args = [
139
+ "deploy",
140
+ "--config",
141
+ cloudflareAgent.configPath,
142
+ ...cloudflareAgent.environment ? ["--env", cloudflareAgent.environment] : []
143
+ ];
144
+ return {
145
+ command: formatCommand$1("wrangler", args),
146
+ cwd: path$1.resolve(root),
147
+ executable: "wrangler",
148
+ args
149
+ };
150
+ }
151
+ const args = [
152
+ "pages",
153
+ "deploy",
154
+ ".",
155
+ `--project-name=${deployConfig.cloudflare?.projectName || deployConfig.projectName || "farm-app"}`
156
+ ];
157
+ return {
158
+ command: formatCommand$1("wrangler", args),
159
+ cwd: outputDir,
160
+ executable: "wrangler",
161
+ args
162
+ };
163
+ }
164
+ function createNetlifyDeployArgs(site) {
165
+ return [
166
+ "deploy",
167
+ "--prod",
168
+ "--dir=.",
169
+ ...site ? [`--site=${site}`] : []
170
+ ];
51
171
  }
52
172
  /**
53
173
  * Deploy using platform's native CLI (user credentials)
@@ -69,19 +189,20 @@ async function deployPlatform(platform, root, outputDir, deployConfig, prod) {
69
189
  async function deployVercel(root, outputDir, prod) {
70
190
  logger.info("🚀 Deploying to Vercel...");
71
191
  try {
72
- execSync("vercel --version", { stdio: "ignore" });
73
- } catch {
74
- logger.error("❌ Vercel CLI is not installed.");
75
- logger.info("💡 Install it with: npm i -g vercel");
76
- process.exit(1);
192
+ execFileSync("vercel", ["--version"], {
193
+ stdio: "ignore",
194
+ cwd: root
195
+ });
196
+ } catch (error) {
197
+ throw new FarmDeployError("CLI_NOT_INSTALLED", "vercel", "Vercel CLI is not installed. Install it with: npm i -g vercel", { cause: error });
77
198
  }
78
199
  try {
79
- execSync("vercel whoami", { stdio: "ignore" });
80
- } catch {
81
- logger.warn("⚠️ Not logged in to Vercel.");
82
- logger.info("💡 Please run: vercel login");
83
- logger.info(" Then run: farm deploy --vercel");
84
- process.exit(1);
200
+ execFileSync("vercel", ["whoami"], {
201
+ stdio: "ignore",
202
+ cwd: root
203
+ });
204
+ } catch (error) {
205
+ throw new FarmDeployError("CLI_NOT_AUTHENTICATED", "vercel", "Vercel CLI is not authenticated. Run 'vercel login', then retry the deployment.", { cause: error });
85
206
  }
86
207
  try {
87
208
  const { existsSync, statSync, readdirSync } = await import("fs");
@@ -90,14 +211,8 @@ async function deployVercel(root, outputDir, prod) {
90
211
  const configFile = path$1.join(outputDir, "config.json");
91
212
  const serverIndex = path$1.join(functionsDir, "index.mjs");
92
213
  logger.info("🔍 Verifying deployment structure...");
93
- if (!existsSync(functionsDir)) {
94
- logger.error(`❌ Functions directory not found at ${functionsDir}`);
95
- process.exit(1);
96
- }
97
- if (!existsSync(serverIndex)) {
98
- logger.error(`❌ Server entry point not found at ${serverIndex}`);
99
- process.exit(1);
100
- }
214
+ if (!existsSync(functionsDir)) throw new FarmDeployError("INVALID_BUILD_OUTPUT", "vercel", `Functions directory not found at ${functionsDir}.`);
215
+ if (!existsSync(serverIndex)) throw new FarmDeployError("INVALID_BUILD_OUTPUT", "vercel", `Server entry point not found at ${serverIndex}.`);
101
216
  logger.info(`✅ Functions directory: ${functionsDir}`);
102
217
  if (!existsSync(staticDir)) logger.warn(`⚠️ Static directory not found at ${staticDir}`);
103
218
  else {
@@ -178,14 +293,19 @@ async function deployVercel(root, outputDir, prod) {
178
293
  logger.info(` Static: ${staticDir}`);
179
294
  logger.info(` Config: ${configFile}`);
180
295
  logger.info("📤 Uploading to Vercel...");
181
- execSync(`vercel deploy --prebuilt --yes${prod ? " --prod" : ""}`, {
296
+ execFileSync("vercel", [
297
+ "deploy",
298
+ "--prebuilt",
299
+ "--yes",
300
+ ...prod ? ["--prod"] : []
301
+ ], {
182
302
  stdio: "inherit",
183
303
  cwd: root
184
304
  });
185
305
  logger.success("✅ Deployed to Vercel successfully!");
186
306
  } catch (error) {
187
- logger.error(`❌ Failed to deploy to Vercel: ${error.message}`);
188
- process.exit(1);
307
+ if (error instanceof FarmDeployError) throw error;
308
+ throw new FarmDeployError("DEPLOY_FAILED", "vercel", `Failed to deploy to Vercel: ${getErrorMessage(error)}`, { cause: error });
189
309
  }
190
310
  }
191
311
  /** Deploy to a composed Worker or fall back to Cloudflare Pages. */
@@ -206,8 +326,7 @@ async function deployCloudflare(root, outputDir, projectName) {
206
326
  });
207
327
  logger.success("✅ Deployed Farm and Cloudflare Agents successfully!");
208
328
  } catch (error) {
209
- logger.error(`❌ Failed to deploy to Cloudflare: ${error.message}`);
210
- process.exit(1);
329
+ throw new FarmDeployError("DEPLOY_FAILED", "cloudflare", `Failed to deploy to Cloudflare: ${getErrorMessage(error)}`, { cause: error });
211
330
  }
212
331
  return;
213
332
  }
@@ -225,8 +344,7 @@ async function deployCloudflare(root, outputDir, projectName) {
225
344
  });
226
345
  logger.success("✅ Deployed to Cloudflare Pages successfully!");
227
346
  } catch (error) {
228
- logger.error(`❌ Failed to deploy to Cloudflare: ${error.message}`);
229
- process.exit(1);
347
+ throw new FarmDeployError("DEPLOY_FAILED", "cloudflare", `Failed to deploy to Cloudflare: ${getErrorMessage(error)}`, { cause: error });
230
348
  }
231
349
  }
232
350
  /** Read the trusted Workers deployment handoff emitted by @farm.js/cf-agent. */
@@ -252,16 +370,34 @@ function resolveCloudflareAgentDeployPlan(root) {
252
370
  ...typeof environment === "string" ? { environment: environment.trim() } : {}
253
371
  };
254
372
  }
373
+ function resolveConfiguredCloudflareAgentDeployPlan(root, integrations) {
374
+ const integration = Object.values(integrations || {}).find((value) => isRecord(value) && value.category === "agent" && value.type === "cloudflare" && value.serverRuntime === false);
375
+ if (!isRecord(integration) || !isRecord(integration.instance)) return void 0;
376
+ const configuredPath = integration.instance.config;
377
+ if (typeof configuredPath !== "string" || !configuredPath.trim()) return void 0;
378
+ const projectRoot = path$1.resolve(root);
379
+ const sourceConfigPath = path$1.resolve(projectRoot, configuredPath);
380
+ assertPathInsideProject(projectRoot, sourceConfigPath, "Cloudflare Agents source config");
381
+ const configPath = path$1.join(path$1.dirname(sourceConfigPath), ".farm-cf-agent.wrangler.jsonc");
382
+ const environment = integration.instance.environment;
383
+ return {
384
+ configPath,
385
+ ...typeof environment === "string" && environment.trim() ? { environment: environment.trim() } : {},
386
+ generated: true
387
+ };
388
+ }
389
+ function assertPathInsideProject(projectRoot, candidate, label) {
390
+ const relativePath = path$1.relative(projectRoot, candidate);
391
+ if (relativePath === ".." || relativePath.startsWith(`..${path$1.sep}`) || path$1.isAbsolute(relativePath)) throw new Error(`${label} must stay inside the Farm project root.`);
392
+ }
255
393
  function assertWranglerInstalled(root) {
256
394
  try {
257
395
  execFileSync("wrangler", ["--version"], {
258
396
  stdio: "ignore",
259
397
  cwd: root
260
398
  });
261
- } catch {
262
- logger.error(" Wrangler CLI is not installed.");
263
- logger.info("💡 Install it in this project with: npm i -D wrangler");
264
- process.exit(1);
399
+ } catch (error) {
400
+ throw new FarmDeployError("CLI_NOT_INSTALLED", "cloudflare", "Wrangler CLI is not installed. Install it in this project with: npm i -D wrangler", { cause: error });
265
401
  }
266
402
  }
267
403
  function isRecord(value) {
@@ -273,20 +409,103 @@ function isRecord(value) {
273
409
  async function deployNetlify(root, outputDir, site) {
274
410
  logger.info("🚀 Deploying to Netlify...");
275
411
  try {
276
- execSync("netlify --version", { stdio: "ignore" });
277
- } catch {
278
- logger.error("❌ Netlify CLI is not installed.");
279
- logger.info("💡 Install it with: npm i -g netlify-cli");
280
- process.exit(1);
412
+ execFileSync("netlify", ["--version"], {
413
+ stdio: "ignore",
414
+ cwd: root
415
+ });
416
+ } catch (error) {
417
+ throw new FarmDeployError("CLI_NOT_INSTALLED", "netlify", "Netlify CLI is not installed. Install it with: npm i -g netlify-cli", { cause: error });
281
418
  }
282
419
  try {
283
- process.chdir(outputDir);
284
- const siteFlag = site ? ` --site=${site}` : "";
285
- execSync(`netlify deploy --prod --dir=.${siteFlag}`, { stdio: "inherit" });
420
+ execFileSync("netlify", createNetlifyDeployArgs(site), {
421
+ stdio: "inherit",
422
+ cwd: outputDir
423
+ });
286
424
  logger.success("✅ Deployed to Netlify successfully!");
287
425
  } catch (error) {
288
- logger.error(`❌ Failed to deploy to Netlify: ${error.message}`);
289
- process.exit(1);
426
+ throw new FarmDeployError("DEPLOY_FAILED", "netlify", `Failed to deploy to Netlify: ${getErrorMessage(error)}`, { cause: error });
427
+ }
428
+ }
429
+ function formatCommand$1(executable, args) {
430
+ return [executable, ...args].map(formatCommandArgument).join(" ");
431
+ }
432
+ const SAFE_ARGUMENT = process.platform === "win32" ? /^[A-Za-z0-9_.\\/:=@+-]+$/ : /^[A-Za-z0-9_./:=@+-]+$/;
433
+ function formatCommandArgument(argument) {
434
+ if (SAFE_ARGUMENT.test(argument)) return argument;
435
+ return `'${argument.replace(/'/g, `'"'"'`)}'`;
436
+ }
437
+ function getErrorMessage(error) {
438
+ return error instanceof Error ? error.message : String(error);
439
+ }
440
+ //#endregion
441
+ //#region src/start.ts
442
+ var FarmStartError = class extends Error {
443
+ constructor(code, message) {
444
+ super(message);
445
+ this.name = "FarmStartError";
446
+ this.code = code;
447
+ }
448
+ };
449
+ const PLATFORM_HINTS = {
450
+ vercel: "Deploy it with `farm deploy --vercel`",
451
+ cloudflare: "Deploy it with `farm deploy --cloudflare`",
452
+ netlify: "Deploy it with `farm deploy --netlify`"
453
+ };
454
+ async function createFarmStartPlan(options = {}) {
455
+ const root = path$1.resolve(options.root || process.cwd());
456
+ const userConfig = await loadConfig(root, void 0, "production");
457
+ const deployConfig = resolveDeployConfig(userConfig || {});
458
+ const target = deployConfig.target;
459
+ const preset = deployConfig.preset || "node-server";
460
+ const outputDir = resolveDeployOutputPath(root, deployConfig.outputDir);
461
+ if (target && target !== "node") throw new FarmStartError("PLATFORM_TARGET", `The "${target}" target builds platform output with no local server to start. ${PLATFORM_HINTS[target]}, or set deploy.target: "node" in farm.config.ts to self-host.`);
462
+ if (!target) throw new FarmStartError("UNSUPPORTED_PRESET", `Preset "${preset}" has no known local server entry. Set deploy.target: "node" in farm.config.ts to self-host.`);
463
+ const serverEntry = path$1.join(outputDir, "server", "index.mjs");
464
+ if (!existsSync$1(serverEntry)) throw new FarmStartError("MISSING_OUTPUT", `No build output found at ${serverEntry}. Run \`farm build\` first.`);
465
+ const env = {};
466
+ if (options.port !== void 0 && options.port !== "") env.NITRO_PORT = String(options.port);
467
+ if (options.host) env.NITRO_HOST = options.host;
468
+ return {
469
+ root,
470
+ target: "node",
471
+ preset,
472
+ outputDir,
473
+ serverEntry,
474
+ command: {
475
+ command: process.execPath,
476
+ args: [serverEntry]
477
+ },
478
+ env
479
+ };
480
+ }
481
+ async function startFarm(options = {}) {
482
+ const plan = await createFarmStartPlan(options);
483
+ logger.info(`Starting Node server: node ${path$1.relative(plan.root, plan.serverEntry)}`);
484
+ const child = spawn(plan.command.command, plan.command.args, {
485
+ cwd: plan.root,
486
+ stdio: "inherit",
487
+ env: {
488
+ ...process.env,
489
+ ...plan.env
490
+ }
491
+ });
492
+ const forward = (signal) => {
493
+ const handler = () => child.kill(signal);
494
+ process.on(signal, handler);
495
+ return () => process.off(signal, handler);
496
+ };
497
+ const cleanups = [forward("SIGINT"), forward("SIGTERM")];
498
+ try {
499
+ await new Promise((resolve, reject) => {
500
+ child.on("error", reject);
501
+ child.on("exit", (code, signal) => {
502
+ if (signal) process.exitCode = 128 + (constants.signals[signal] ?? 1);
503
+ else if (code !== null) process.exitCode = code;
504
+ resolve();
505
+ });
506
+ });
507
+ } finally {
508
+ for (const cleanup of cleanups) cleanup();
290
509
  }
291
510
  }
292
511
  //#endregion
@@ -311,10 +530,12 @@ const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
311
530
  function createPreviewGatewayPlan(target, options = {}) {
312
531
  const requestedName = sanitizePreviewName$1(options.name || process.env.FARM_PREVIEW_NAME) || randomPreviewName$1();
313
532
  const gatewayUrl = normalizeGatewayUrl(options.gatewayUrl || process.env.FARM_PREVIEW_GATEWAY_URL || DEFAULT_GATEWAY_URL);
533
+ const relayUrl = normalizeRelayUrl(process.env.FARM_PREVIEW_RELAY_URL || gatewayUrl);
314
534
  const requestedHostname = `${requestedName}.${normalizePreviewDomain$1(process.env.FARM_PREVIEW_DOMAIN || DEFAULT_PREVIEW_DOMAIN)}`;
315
535
  return {
316
536
  provider: "farm-gateway",
317
537
  gatewayUrl,
538
+ relayUrl,
318
539
  target,
319
540
  requestedName,
320
541
  requestedHostname,
@@ -423,6 +644,9 @@ async function createGatewaySession(plan, timeoutMs) {
423
644
  if (!session.id || !session.token || !session.publicUrl) throw new Error("Preview gateway returned an invalid session.");
424
645
  return session;
425
646
  }
647
+ function getSetCookies(headers) {
648
+ return headers.getSetCookie?.call(headers) || [];
649
+ }
426
650
  async function forwardGatewayRequest(target, request) {
427
651
  const headers = new Headers();
428
652
  for (const [key, value] of Object.entries(request.headers || {})) {
@@ -441,8 +665,10 @@ async function forwardGatewayRequest(target, request) {
441
665
  const responseHeaders = {};
442
666
  response.headers.forEach((value, key) => {
443
667
  const normalized = key.toLowerCase();
444
- if (!HOP_BY_HOP_HEADERS.has(normalized)) responseHeaders[key] = value;
668
+ if (!HOP_BY_HOP_HEADERS.has(normalized) && normalized !== "set-cookie") responseHeaders[key] = value;
445
669
  });
670
+ const setCookies = getSetCookies(response.headers);
671
+ if (setCookies.length > 0) responseHeaders["set-cookie"] = setCookies;
446
672
  return {
447
673
  status: response.status,
448
674
  headers: responseHeaders,
@@ -479,6 +705,7 @@ async function closeGatewaySession(plan, session) {
479
705
  function formatGatewayPlan(plan) {
480
706
  return [
481
707
  `Gateway: ${plan.gatewayUrl}`,
708
+ `Relay: ${plan.relayUrl}`,
482
709
  `Local: ${plan.target.localUrl}`,
483
710
  `Public: ${plan.requestedPublicUrl}`
484
711
  ].join("\n");
@@ -490,6 +717,17 @@ function formatRequestPath(path) {
490
717
  function normalizeGatewayUrl(value) {
491
718
  return value.replace(/\/+$/, "");
492
719
  }
720
+ function normalizeRelayUrl(value) {
721
+ const url = new URL(value);
722
+ if (url.protocol === "http:") url.protocol = "ws:";
723
+ if (url.protocol === "https:") url.protocol = "wss:";
724
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new Error(`Preview relay must use ws or wss, received ${url.protocol}`);
725
+ const pathname = url.pathname.replace(/\/+$/, "");
726
+ url.pathname = pathname.endsWith("/agent") ? pathname : `${pathname}/agent`;
727
+ url.search = "";
728
+ url.hash = "";
729
+ return url.toString();
730
+ }
493
731
  function normalizePreviewDomain$1(value) {
494
732
  return value.replace(/^https?:\/\//, "").replace(/^\.*/, "").replace(/\/*$/, "");
495
733
  }
@@ -501,6 +739,39 @@ function randomPreviewName$1() {
501
739
  return `farm-${Math.random().toString(36).slice(2, 8)}`;
502
740
  }
503
741
  //#endregion
742
+ //#region src/preview-native.ts
743
+ async function runNativePreviewTunnel(plan, options = {}) {
744
+ const runtime = options.runtime || await loadNativeTunnel();
745
+ const session = await runtime.startPreviewAgent(plan.relayUrl, plan.requestedName, plan.target.localUrl);
746
+ let stopping = false;
747
+ const stop = () => {
748
+ if (stopping) return;
749
+ stopping = true;
750
+ runtime.stopPreviewAgent(session.sessionId);
751
+ };
752
+ process.once("SIGINT", stop);
753
+ process.once("SIGTERM", stop);
754
+ logger.success("Preview URL ready.");
755
+ logger.info(`Public: ${session.publicUrl}`);
756
+ logger.info("Forwarding requests through the native tunnel until Ctrl+C.");
757
+ try {
758
+ if (!await runtime.waitPreviewAgent(session.sessionId)) throw new Error("The native preview tunnel stopped before its lifecycle could be observed.");
759
+ return session;
760
+ } finally {
761
+ process.removeListener("SIGINT", stop);
762
+ process.removeListener("SIGTERM", stop);
763
+ await runtime.stopPreviewAgent(session.sessionId).catch(() => false);
764
+ }
765
+ }
766
+ async function loadNativeTunnel() {
767
+ try {
768
+ return await import("@farm.js/tunnel");
769
+ } catch (error) {
770
+ const reason = error instanceof Error ? ` ${error.message}` : "";
771
+ throw new Error(`Could not load @farm.js/tunnel for this platform. Reinstall @farm.js/cli so its native platform package is restored.${reason}`);
772
+ }
773
+ }
774
+ //#endregion
504
775
  //#region src/preview.ts
505
776
  const DEFAULT_PREVIEW_PORTS = [
506
777
  3e3,
@@ -524,15 +795,26 @@ async function previewFarm(options = {}) {
524
795
  plan
525
796
  };
526
797
  }
527
- logger.info(`Gateway: ${plan.gatewayUrl}`);
528
- logger.info("Opening Farm preview gateway session...");
529
- const session = await runPreviewGateway(plan, { timeoutMs: options.timeoutMs });
530
- return {
531
- target,
532
- plan,
533
- publicUrl: session.publicUrl,
534
- session
535
- };
798
+ logger.info(`Relay: ${plan.relayUrl}`);
799
+ logger.info("Opening native Farm preview tunnel...");
800
+ try {
801
+ const session = await runNativePreviewTunnel(plan);
802
+ return {
803
+ target,
804
+ plan,
805
+ publicUrl: session.publicUrl,
806
+ session
807
+ };
808
+ } catch (error) {
809
+ logger.warn(`Native preview relay unavailable; using compatibility gateway polling.${formatPreviewError(error)}`);
810
+ const session = await runPreviewGateway(plan, { timeoutMs: options.timeoutMs });
811
+ return {
812
+ target,
813
+ plan,
814
+ publicUrl: session.publicUrl,
815
+ session
816
+ };
817
+ }
536
818
  }
537
819
  const plan = createPreviewTunnelPlan(target, options);
538
820
  if (options.dryRun) {
@@ -550,6 +832,9 @@ async function previewFarm(options = {}) {
550
832
  publicUrl: await runPreviewTunnel(plan, options.timeoutMs ?? 3e4)
551
833
  };
552
834
  }
835
+ function formatPreviewError(error) {
836
+ return error instanceof Error && error.message ? ` ${error.message}` : "";
837
+ }
553
838
  function shouldUseManagedGateway(options) {
554
839
  if (options.provider === "farm" || process.env.FARM_PREVIEW_PROVIDER === "farm") return true;
555
840
  if (options.provider === "local" || process.env.FARM_PREVIEW_PROVIDER === "local") return false;
@@ -658,7 +943,7 @@ function parsePreviewPublicUrl(output, preferredHostname) {
658
943
  }) || urls[0];
659
944
  }
660
945
  async function runPreviewTunnel(plan, timeoutMs) {
661
- const child = spawn(plan.command, plan.args, {
946
+ const child = spawn$1(plan.command, plan.args, {
662
947
  env: {
663
948
  ...process.env,
664
949
  FARM_PREVIEW_LOCAL_URL: plan.target.localUrl,
@@ -800,10 +1085,20 @@ function normalizePreviewDomain(value) {
800
1085
  }
801
1086
  //#endregion
802
1087
  //#region src/generate.ts
1088
+ var FarmGeneratedArtifactsStaleError = class extends Error {
1089
+ constructor(root, stalePaths) {
1090
+ const normalizedPaths = [...new Set(stalePaths)].sort();
1091
+ const relativePaths = normalizedPaths.map((filePath) => path.relative(root, filePath));
1092
+ super(`Generated types are stale:\n${relativePaths.map((filePath) => ` - ${filePath}`).join("\n")}\nRun farm generate and commit the updated files.`);
1093
+ this.name = "FarmGeneratedArtifactsStaleError";
1094
+ this.stalePaths = normalizedPaths;
1095
+ }
1096
+ };
803
1097
  const PRISMA_GENERATED_START = "// Farm.js integrations generated schema: start";
804
1098
  const PRISMA_GENERATED_END = "// Farm.js integrations generated schema: end";
805
1099
  async function generateFarmArtifacts(options = {}) {
806
1100
  const root = path.resolve(options.root || process.cwd());
1101
+ if (options.check && hasSchemaOptions(options)) throw new Error("--check verifies generated framework types and cannot be combined with schema output options.");
807
1102
  const userConfig = await loadConfig(root, options.configPath, "development");
808
1103
  if (!userConfig && hasSchemaOptions(options)) throw new Error("No Farm config found. Please create farm.config.ts or config.ts.");
809
1104
  const resolvedConfig = await resolveConfig({
@@ -818,14 +1113,21 @@ async function generateFarmArtifacts(options = {}) {
818
1113
  layers: resolvedConfig.layers,
819
1114
  extraRoutes,
820
1115
  suppressLintOnLink: resolvedConfig.suppressLintOnLink,
821
- i18nConfig: resolvedConfig.i18n
1116
+ componentExtensions: resolvedConfig.renderer.componentExtensions,
1117
+ i18nConfig: resolvedConfig.i18n,
1118
+ check: options.check
822
1119
  });
1120
+ if (options.check) {
1121
+ if (typeArtifacts.stalePaths.length) throw new FarmGeneratedArtifactsStaleError(root, typeArtifacts.stalePaths);
1122
+ logger.success(`Generated route, API, env${resolvedConfig.i18n.enabled ? ", and i18n" : ""} types are up to date.`);
1123
+ return typeArtifacts;
1124
+ }
823
1125
  logger.success(`Generated route, API, env${resolvedConfig.i18n.enabled ? ", and i18n" : ""} types (${typeArtifacts.apiRoutes.length} API route${typeArtifacts.apiRoutes.length === 1 ? "" : "s"}).`);
824
1126
  const schemas = getIntegrationSchemas(resolvedConfig.integrations);
825
1127
  const schemaEntries = Object.entries(schemas);
826
1128
  if (!schemaEntries.length) {
827
1129
  if (hasSchemaOptions(options)) logger.warn("No integration schemas were found in the current Farm config.");
828
- return;
1130
+ return typeArtifacts;
829
1131
  }
830
1132
  const packageManifest = await readPackageManifest(root);
831
1133
  const schemaOptionsExplicit = hasSchemaOptions(options);
@@ -835,12 +1137,12 @@ async function generateFarmArtifacts(options = {}) {
835
1137
  } catch (error) {
836
1138
  if (schemaOptionsExplicit) throw error;
837
1139
  logger.warn(`Integration schemas were found, but Farm could not choose a schema target automatically: ${error.message}`);
838
- return;
1140
+ return typeArtifacts;
839
1141
  }
840
1142
  if (!orm) {
841
1143
  if (!schemaOptionsExplicit) {
842
1144
  logger.warn("Integration schemas were found, but no data layer was detected. Pass --orm prisma|drizzle|postgres|mysql|sqlite|mongodb to generate schema artifacts.");
843
- return;
1145
+ return typeArtifacts;
844
1146
  }
845
1147
  throw new Error("Could not auto-detect a schema target. Pass one explicitly with --orm prisma|drizzle|postgres|mysql|sqlite|mongodb.");
846
1148
  }
@@ -851,7 +1153,7 @@ async function generateFarmArtifacts(options = {}) {
851
1153
  if (!existsSync(schemaPath)) throw new Error(`Prisma target was selected but no schema file was found at ${schemaPath}. Create prisma/schema.prisma or pass --output.`);
852
1154
  await writePrismaSchema(schemaPath, collectedModels);
853
1155
  logger.success(`Generated Prisma integration schema in ${path.relative(root, schemaPath)}.`);
854
- return;
1156
+ return typeArtifacts;
855
1157
  }
856
1158
  case "drizzle": {
857
1159
  const dialect = options.dialect ?? await detectDrizzleDialect(root, packageManifest) ?? void 0;
@@ -859,7 +1161,7 @@ async function generateFarmArtifacts(options = {}) {
859
1161
  const outputPath = options.output ? path.resolve(root, options.output) : path.join(root, "farm-integrations.generated.ts");
860
1162
  await writeGeneratedFile(outputPath, generateDrizzleSchema(collectedModels, dialect));
861
1163
  logger.success(`Generated Drizzle integration schema in ${path.relative(root, outputPath)}.`);
862
- return;
1164
+ return typeArtifacts;
863
1165
  }
864
1166
  case "postgres":
865
1167
  case "mysql":
@@ -867,13 +1169,13 @@ async function generateFarmArtifacts(options = {}) {
867
1169
  const outputPath = options.output ? path.resolve(root, options.output) : path.join(root, `farm-integrations.generated.${orm}.sql`);
868
1170
  await writeGeneratedFile(outputPath, generateSqlSchema(collectedModels, orm));
869
1171
  logger.success(`Generated ${orm} integration schema in ${path.relative(root, outputPath)}.`);
870
- return;
1172
+ return typeArtifacts;
871
1173
  }
872
1174
  case "mongodb": {
873
1175
  const outputPath = options.output ? path.resolve(root, options.output) : path.join(root, "farm-integrations.generated.mongodb.ts");
874
1176
  await writeGeneratedFile(outputPath, generateMongoBootstrap(collectedModels));
875
1177
  logger.success(`Generated MongoDB integration bootstrap in ${path.relative(root, outputPath)}.`);
876
- return;
1178
+ return typeArtifacts;
877
1179
  }
878
1180
  }
879
1181
  }
@@ -1058,7 +1360,7 @@ function cloneSchemaModel(model) {
1058
1360
  async function writePrismaSchema(schemaPath, models) {
1059
1361
  const source = await readFile(schemaPath, "utf8");
1060
1362
  const generated = createPrismaGeneratedBlock(generatePrismaSchema(models));
1061
- const pattern = new RegExp(`${escapeRegExp(PRISMA_GENERATED_START)}[\\s\\S]*?${escapeRegExp(PRISMA_GENERATED_END)}`, "m");
1363
+ const pattern = new RegExp(`${escapeRegExp$1(PRISMA_GENERATED_START)}[\\s\\S]*?${escapeRegExp$1(PRISMA_GENERATED_END)}`, "m");
1062
1364
  const nextSource = pattern.test(source) ? source.replace(pattern, generated) : `${source.trimEnd()}\n\n${generated}\n`;
1063
1365
  await writeFile(schemaPath, nextSource, "utf8");
1064
1366
  }
@@ -1080,18 +1382,18 @@ function renderPrismaModel(model) {
1080
1382
  const defaultAttribute = getPrismaDefaultAttribute(field);
1081
1383
  if (defaultAttribute) attributes.push(defaultAttribute);
1082
1384
  if (field.meta?.autoUpdate && field.type === "datetime") attributes.push("@updatedAt");
1083
- if (field.name !== fieldKey) attributes.push(`@map("${escapeString(field.name)}")`);
1385
+ if (field.name !== fieldKey) attributes.push(`@map("${escapeDoubleQuoted(field.name)}")`);
1084
1386
  if (attributes.length) parts.push(attributes.join(" "));
1085
1387
  lines.push(` ${parts.join(" ")}`);
1086
- if (field.index) modelLevelConstraints.push(`@@index([${fieldKey}], map: "${escapeString(`${model.modelName}_${field.name}_idx`)}")`);
1388
+ if (field.index) modelLevelConstraints.push(`@@index([${fieldKey}], map: "${escapeDoubleQuoted(`${model.modelName}_${field.name}_idx`)}")`);
1087
1389
  }
1088
1390
  for (const constraint of model.model.constraints || []) {
1089
1391
  const fields = constraint.fields.join(", ");
1090
1392
  const attribute = constraint.type === "unique" ? "@@unique" : "@@index";
1091
- const suffix = constraint.name ? `, map: "${escapeString(constraint.name)}"` : "";
1393
+ const suffix = constraint.name ? `, map: "${escapeDoubleQuoted(constraint.name)}"` : "";
1092
1394
  modelLevelConstraints.push(`${attribute}([${fields}]${suffix})`);
1093
1395
  }
1094
- lines.push(` @@map("${escapeString(model.modelName)}")`);
1396
+ lines.push(` @@map("${escapeDoubleQuoted(model.modelName)}")`);
1095
1397
  for (const constraint of modelLevelConstraints) lines.push(` ${constraint}`);
1096
1398
  lines.push("}");
1097
1399
  return lines.join("\n");
@@ -1109,7 +1411,7 @@ function getPrismaFieldType(field) {
1109
1411
  function getPrismaDefaultAttribute(field) {
1110
1412
  if (field.default === void 0) return null;
1111
1413
  if (field.type === "datetime" && field.default === "now") return "@default(now())";
1112
- if (typeof field.default === "string") return `@default("${escapeString(field.default)}")`;
1414
+ if (typeof field.default === "string") return `@default("${escapeDoubleQuoted(field.default)}")`;
1113
1415
  if (typeof field.default === "number" || typeof field.default === "boolean") return `@default(${String(field.default)})`;
1114
1416
  return null;
1115
1417
  }
@@ -1147,12 +1449,12 @@ function renderDrizzleModel(model, dialect, tableFactoryName) {
1147
1449
  lines.push(` ${fieldKey}: ${renderDrizzleColumn(field, dialect)},`);
1148
1450
  }
1149
1451
  lines.push("}, (table) => ({");
1150
- for (const [fieldKey, field] of Object.entries(model.model.fields)) if (field.index) lines.push(` ${fieldKey}Idx: index("${escapeString(`${model.modelName}_${field.name}_idx`)}").on(table.${fieldKey}),`);
1452
+ for (const [fieldKey, field] of Object.entries(model.model.fields)) if (field.index) lines.push(` ${fieldKey}Idx: index("${escapeDoubleQuoted(`${model.modelName}_${field.name}_idx`)}").on(table.${fieldKey}),`);
1151
1453
  for (const constraint of model.model.constraints || []) {
1152
1454
  const builder = constraint.type === "unique" ? "uniqueIndex" : "index";
1153
1455
  const accessor = constraint.fields.map((fieldKey) => `table.${fieldKey}`).join(", ");
1154
1456
  const name = constraint.name || `${model.modelName}_${constraint.fields.map((fieldKey) => model.model.fields[fieldKey]?.name || fieldKey).join("_")}_${constraint.type}`;
1155
- lines.push(` ${toCamelCase(name)}: ${builder}("${escapeString(name)}").on(${accessor}),`);
1457
+ lines.push(` ${toCamelCase(name)}: ${builder}("${escapeDoubleQuoted(name)}").on(${accessor}),`);
1156
1458
  }
1157
1459
  lines.push("}));");
1158
1460
  return lines.join("\n");
@@ -1230,7 +1532,7 @@ function renderSqliteDrizzleColumn(field) {
1230
1532
  function getDrizzleDefaultExpression(field, dialect) {
1231
1533
  if (field.default === void 0) return "";
1232
1534
  if (field.type === "datetime" && field.default === "now") return dialect === "sqlite" ? "" : ".defaultNow()";
1233
- if (typeof field.default === "string") return `.default("${escapeString(field.default)}")`;
1535
+ if (typeof field.default === "string") return `.default("${escapeDoubleQuoted(field.default)}")`;
1234
1536
  if (typeof field.default === "number" || typeof field.default === "boolean") return `.default(${String(field.default)})`;
1235
1537
  return "";
1236
1538
  }
@@ -1321,7 +1623,7 @@ function getSqlColumnType(field, dialect) {
1321
1623
  function getSqlDefaultExpression(field, dialect) {
1322
1624
  if (field.default === void 0) return null;
1323
1625
  if (field.type === "datetime" && field.default === "now") return "CURRENT_TIMESTAMP";
1324
- if (typeof field.default === "string") return `'${escapeString(field.default)}'`;
1626
+ if (typeof field.default === "string") return `'${escapeSqlString(field.default)}'`;
1325
1627
  if (typeof field.default === "number") return String(field.default);
1326
1628
  if (typeof field.default === "boolean") {
1327
1629
  if (dialect === "sqlite") return field.default ? "1" : "0";
@@ -1338,20 +1640,20 @@ function generateMongoBootstrap(models) {
1338
1640
  ];
1339
1641
  for (const model of models) {
1340
1642
  lines.push(` // Integration "${model.integrationKey}" model "${model.modelKey}"`);
1341
- lines.push(` const ${model.exportName} = db.collection("${escapeString(model.modelName)}");`);
1643
+ lines.push(` const ${model.exportName} = db.collection("${escapeDoubleQuoted(model.modelName)}");`);
1342
1644
  for (const [fieldKey, field] of Object.entries(model.model.fields)) {
1343
1645
  if (field.unique) {
1344
1646
  const options = ["unique: true"];
1345
1647
  if (isNullableField(field)) options.push("sparse: true");
1346
- options.push(`name: "${escapeString(`${model.modelName}_${field.name}_unique`)}"`);
1648
+ options.push(`name: "${escapeDoubleQuoted(`${model.modelName}_${field.name}_unique`)}"`);
1347
1649
  lines.push(` await ${model.exportName}.createIndex({ ${JSON.stringify(field.name)}: 1 }, { ${options.join(", ")} });`);
1348
- } else if (field.index) lines.push(` await ${model.exportName}.createIndex({ ${JSON.stringify(field.name)}: 1 }, { name: "${escapeString(`${model.modelName}_${field.name}_idx`)}" });`);
1650
+ } else if (field.index) lines.push(` await ${model.exportName}.createIndex({ ${JSON.stringify(field.name)}: 1 }, { name: "${escapeDoubleQuoted(`${model.modelName}_${field.name}_idx`)}" });`);
1349
1651
  if (field.reference) lines.push(` // ${fieldKey} references ${field.reference.model}.${field.reference.field}${field.reference.onDelete ? ` (onDelete: ${field.reference.onDelete})` : ""}`);
1350
1652
  }
1351
1653
  for (const constraint of model.model.constraints || []) {
1352
1654
  const indexSpec = constraint.fields.map((fieldKey) => `${JSON.stringify(model.model.fields[fieldKey]?.name || fieldKey)}: 1`).join(", ");
1353
1655
  const indexName = constraint.name || `${model.modelName}_${constraint.fields.map((fieldKey) => model.model.fields[fieldKey]?.name || fieldKey).join("_")}_${constraint.type}`;
1354
- const options = constraint.type === "unique" ? `{ unique: true, name: "${escapeString(indexName)}" }` : `{ name: "${escapeString(indexName)}" }`;
1656
+ const options = constraint.type === "unique" ? `{ unique: true, name: "${escapeDoubleQuoted(indexName)}" }` : `{ name: "${escapeDoubleQuoted(indexName)}" }`;
1355
1657
  lines.push(` await ${model.exportName}.createIndex({ ${indexSpec} }, ${options});`);
1356
1658
  }
1357
1659
  lines.push("");
@@ -1383,19 +1685,23 @@ function toCamelCase(value) {
1383
1685
  const pascal = toPascalCase(value);
1384
1686
  return pascal ? pascal.charAt(0).toLowerCase() + pascal.slice(1) : pascal;
1385
1687
  }
1386
- function escapeString(value) {
1387
- return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/'/g, "''");
1688
+ function escapeDoubleQuoted(value) {
1689
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1388
1690
  }
1389
- function escapeRegExp(value) {
1691
+ function escapeSqlString(value) {
1692
+ return value.replace(/'/g, "''");
1693
+ }
1694
+ function escapeRegExp$1(value) {
1390
1695
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1391
1696
  }
1392
1697
  //#endregion
1393
1698
  //#region src/doctor.ts
1394
- const ROUTE_EXTENSIONS = [
1699
+ const ROUTE_EXTENSIONS$1 = [
1395
1700
  "ts",
1396
1701
  "tsx",
1397
1702
  "js",
1398
1703
  "jsx",
1704
+ "vue",
1399
1705
  "md",
1400
1706
  "mdx"
1401
1707
  ];
@@ -1413,7 +1719,7 @@ async function runFarmDoctor(options = {}) {
1413
1719
  const root = path.resolve(options.root || process.cwd());
1414
1720
  const liveTarget = resolveLiveTarget(options);
1415
1721
  let liveError;
1416
- if (!options.offline) try {
1722
+ if (!options.offline && !options.fix) try {
1417
1723
  return createLiveReport(await fetchLiveSnapshot(liveTarget, options), liveTarget, options.now);
1418
1724
  } catch (error) {
1419
1725
  liveError = formatError$2(error);
@@ -1460,6 +1766,13 @@ function formatFarmDoctorReport(report, options = {}) {
1460
1766
  `${report.summary.fail} failed`,
1461
1767
  `${report.summary.info} info`
1462
1768
  ].join(" / ");
1769
+ if (report.fixes?.length) {
1770
+ lines.push("", color.bold("FIXED"));
1771
+ for (const fix of report.fixes) {
1772
+ lines.push(` ${color.green("✓")} ${fix.title}`);
1773
+ lines.push(` ${color.dim(fix.filePath)}`);
1774
+ }
1775
+ }
1463
1776
  lines.push("", `${color.bold("SUMMARY")} ${summary}`);
1464
1777
  if (report.target?.devtoolsUrl) lines.push(`${color.bold("DEVTOOLS")} ${report.target.devtoolsUrl}`);
1465
1778
  return lines.join("\n");
@@ -1486,6 +1799,10 @@ async function fetchLiveSnapshot(baseUrl, options) {
1486
1799
  clearTimeout(timeout);
1487
1800
  }
1488
1801
  }
1802
+ /** Reported paths are shown to the user, so keep them POSIX on every platform. */
1803
+ function toPosix$1(value) {
1804
+ return value.split(path.sep).join("/");
1805
+ }
1489
1806
  function createLiveReport(snapshot, baseUrl, now) {
1490
1807
  const checks = [
1491
1808
  {
@@ -1559,16 +1876,17 @@ async function createProjectReport(root, options) {
1559
1876
  action: "Add farm.config.ts and export defineConfig({...})."
1560
1877
  });
1561
1878
  else {
1879
+ const configRoot = path.resolve(root, userConfig.root || ".");
1562
1880
  config = await resolveConfig({
1563
- root,
1564
- ...userConfig
1881
+ ...userConfig,
1882
+ root: configRoot
1565
1883
  }, "development");
1566
1884
  const configFile = findConfigFile(root, options.configPath);
1567
1885
  checks.push({
1568
1886
  status: "pass",
1569
1887
  code: "CONFIG_VALID",
1570
1888
  title: "Farm config loads successfully",
1571
- message: configFile ? path.relative(root, configFile) || path.basename(configFile) : "Resolved config"
1889
+ message: configFile ? toPosix$1(path.relative(root, configFile)) || path.basename(configFile) : "Resolved config"
1572
1890
  });
1573
1891
  }
1574
1892
  } catch (error) {
@@ -1585,9 +1903,41 @@ async function createProjectReport(root, options) {
1585
1903
  report.target = collectDeploymentChecks(config, userConfig, checks);
1586
1904
  collectCronChecks(config, options.env || process.env, checks);
1587
1905
  }
1906
+ if (config && options.fix) {
1907
+ const fixes = applySafeProjectFixes(root, config, checks);
1908
+ if (fixes.length) {
1909
+ const refreshed = await createProjectReport(root, {
1910
+ ...options,
1911
+ fix: false
1912
+ });
1913
+ refreshed.fixes = fixes;
1914
+ return refreshed;
1915
+ }
1916
+ report.fixes = [];
1917
+ }
1588
1918
  finalizeReport(report);
1589
1919
  return report;
1590
1920
  }
1921
+ function applySafeProjectFixes(root, config, checks) {
1922
+ const fixes = [];
1923
+ if (checks.some((check) => check.code === "ROOT_LAYOUT_MISSING")) {
1924
+ const rendererExtension = config.renderer.componentExtensions?.[0] || ".tsx";
1925
+ const layoutPath = path.join(config.root, config.srcDir, "app", `layout${rendererExtension}`);
1926
+ if (!existsSync(layoutPath)) {
1927
+ mkdirSync(path.dirname(layoutPath), { recursive: true });
1928
+ writeFileSync(layoutPath, config.renderer.name === "vue" ? `<script setup lang="ts">\ndefineOptions({ inheritAttrs: false });\n<\/script>\n\n<template>\n <slot />\n</template>\n` : config.renderer.name === "solid" ? `import type { ParentProps } from "solid-js";\n\nexport default function RootLayout(props: ParentProps) {\n return <>{props.children}</>;\n}\n` : `import type { ReactNode } from "react";\n\nexport default function RootLayout({ children }: { children: ReactNode }) {\n return (\n <html lang="en">\n <body>{children}</body>\n </html>\n );\n}\n`, {
1929
+ encoding: "utf8",
1930
+ flag: "wx"
1931
+ });
1932
+ fixes.push({
1933
+ code: "ROOT_LAYOUT_CREATED",
1934
+ title: "Created the missing root layout",
1935
+ filePath: toPosix$1(path.relative(root, layoutPath))
1936
+ });
1937
+ }
1938
+ }
1939
+ return fixes;
1940
+ }
1591
1941
  function collectNodeCheck(checks) {
1592
1942
  const major = Number(process.versions.node.split(".")[0]);
1593
1943
  checks.push(major >= 18 ? {
@@ -1647,8 +1997,8 @@ function collectPackageCheck(root, checks) {
1647
1997
  function collectRouterChecks(config, checks) {
1648
1998
  const sources = getFarmSourceRoots(config);
1649
1999
  const appDirectories = sources.map((source) => path.join(source.root, source.srcDir, "app"));
1650
- const hasPages = appDirectories.some((directory) => containsFile(directory, /^page\.(?:ts|tsx|js|jsx|md|mdx)$/));
1651
- const hasProgrammaticRoutes = sources.some((source) => ROUTE_EXTENSIONS.some((extension) => existsSync(path.join(source.root, source.srcDir, `farm.routes.${extension}`))));
2000
+ const hasPages = appDirectories.some((directory) => containsFile(directory, /^page\.(?:ts|tsx|js|jsx|vue|md|mdx)$/));
2001
+ const hasProgrammaticRoutes = sources.some((source) => ROUTE_EXTENSIONS$1.some((extension) => existsSync(path.join(source.root, source.srcDir, `farm.routes.${extension}`))));
1652
2002
  checks.push(hasPages || hasProgrammaticRoutes ? {
1653
2003
  status: "pass",
1654
2004
  code: "APP_ROUTER_READY",
@@ -1661,7 +2011,8 @@ function collectRouterChecks(config, checks) {
1661
2011
  message: `Farm found no page modules under ${config.srcDir}/app.`,
1662
2012
  action: `Add ${config.srcDir}/app/page.tsx or ${config.srcDir}/farm.routes.tsx.`
1663
2013
  });
1664
- const hasRootLayout = appDirectories.some((directory) => ROUTE_EXTENSIONS.some((extension) => existsSync(path.join(directory, `layout.${extension}`))));
2014
+ const hasRootLayout = appDirectories.some((directory) => ROUTE_EXTENSIONS$1.some((extension) => existsSync(path.join(directory, `layout.${extension}`))));
2015
+ const suggestedLayoutExtension = config.renderer.componentExtensions?.[0] || ".tsx";
1665
2016
  checks.push(hasRootLayout ? {
1666
2017
  status: "pass",
1667
2018
  code: "ROOT_LAYOUT_READY",
@@ -1672,7 +2023,7 @@ function collectRouterChecks(config, checks) {
1672
2023
  code: "ROOT_LAYOUT_MISSING",
1673
2024
  title: "Root layout is missing",
1674
2025
  message: "The application has no shared root layout.",
1675
- action: `Add ${config.srcDir}/app/layout.tsx.`
2026
+ action: `Add ${config.srcDir}/app/layout${suggestedLayoutExtension}.`
1676
2027
  });
1677
2028
  }
1678
2029
  function collectDeploymentChecks(config, userConfig, checks) {
@@ -1731,7 +2082,7 @@ function hasCronRoute(config, job) {
1731
2082
  const relative = job.path.replace(/^\/+/, "").replace(/^api\//, "");
1732
2083
  return getFarmSourceRoots(config).some((source) => {
1733
2084
  const directory = path.join(source.root, source.srcDir, "app", "api", relative);
1734
- return ROUTE_EXTENSIONS.some((extension) => existsSync(path.join(directory, `route.${extension}`)));
2085
+ return ROUTE_EXTENSIONS$1.some((extension) => existsSync(path.join(directory, `route.${extension}`)));
1735
2086
  });
1736
2087
  }
1737
2088
  function containsFile(directory, pattern) {
@@ -1799,6 +2150,415 @@ function formatCount$1(value, noun) {
1799
2150
  return `${value} ${noun}${value === 1 ? "" : "s"}`;
1800
2151
  }
1801
2152
  //#endregion
2153
+ //#region src/explain.ts
2154
+ const ROUTE_EXTENSIONS = [
2155
+ "tsx",
2156
+ "ts",
2157
+ "jsx",
2158
+ "js",
2159
+ "vue",
2160
+ "mdx",
2161
+ "md"
2162
+ ];
2163
+ const MIDDLEWARE_EXTENSIONS = [
2164
+ "ts",
2165
+ "tsx",
2166
+ "js",
2167
+ "jsx",
2168
+ "mjs",
2169
+ "cjs"
2170
+ ];
2171
+ const SOCIAL_IMAGE_EXTENSIONS = [
2172
+ "tsx",
2173
+ "ts",
2174
+ "jsx",
2175
+ "js",
2176
+ "png",
2177
+ "jpg",
2178
+ "jpeg",
2179
+ "gif",
2180
+ "webp"
2181
+ ];
2182
+ async function explainFarmRoute(pathname, options = {}) {
2183
+ const root = path.resolve(options.root || process.cwd());
2184
+ const userConfig = await loadConfig(root, options.configPath, "production");
2185
+ const config = await resolveConfig({
2186
+ root,
2187
+ ...userConfig
2188
+ }, "production");
2189
+ const normalizedPathname = normalizePathname(pathname, config.basePath || "/");
2190
+ const page = discoverMatchingPages(config, normalizedPathname).sort((left, right) => right.score - left.score || right.priority - left.priority)[0];
2191
+ if (!page) throw new Error(`No Farm page route matches ${normalizedPathname}.`);
2192
+ const layouts = collectInheritedRouteFiles(config, normalizedPathname, "layout", ROUTE_EXTENSIONS);
2193
+ const middleware = collectMiddleware(root, Boolean(userConfig?.middleware && Object.keys(userConfig.middleware).length), config, normalizedPathname);
2194
+ const pageSource = readFileSync(page.filePath, "utf8");
2195
+ const layoutSources = layouts.map((filePath) => ({
2196
+ filePath,
2197
+ source: readFileSync(filePath, "utf8")
2198
+ }));
2199
+ const runtime = resolveFarmRouteRuntimeConfig(mergeFarmRouteRuntimeConfigs(resolveFarmRouteRuleRuntimeConfig(normalizedPathname, config.routeRules), ...layoutSources.map(({ source }) => readRuntimeExports(source)), readRuntimeExports(pageSource)), `Route ${page.pattern}`);
2200
+ const matchingRules = Object.entries(config.routeRules).filter(([pattern]) => farmRouteRuleMatches(pattern, normalizedPathname)).sort(([left], [right]) => routeSpecificity(left) - routeSpecificity(right));
2201
+ const rendering = resolveRendering(pageSource, matchingRules);
2202
+ const cache = resolveCaching(pageSource, matchingRules);
2203
+ const metadataSources = [...layoutSources, {
2204
+ filePath: page.filePath,
2205
+ source: pageSource
2206
+ }];
2207
+ const openGraphImage = findNearestSocialImage(config, normalizedPathname, "opengraph-image");
2208
+ const twitterImage = findNearestSocialImage(config, normalizedPathname, "twitter-image");
2209
+ const preset = String(config.deploy.preset || config.preset || "node-server");
2210
+ const presetRuntime = getFarmPresetRuntime(preset);
2211
+ const compatible = rendering.mode === "static" || rendering.mode === "client" || runtime.runtime === "auto" || presetRuntime !== "unknown" && runtime.runtime === presetRuntime;
2212
+ const warnings = [];
2213
+ if (presetRuntime === "unknown" && runtime.runtime !== "auto") warnings.push(`Farm cannot verify the ${runtime.runtime} route requirement because the ${preset} preset runtime is unknown.`);
2214
+ else if (!compatible) warnings.push(`The route requires ${runtime.runtime}, but the ${preset} preset emits ${presetRuntime} functions.`);
2215
+ if (runtime.regions?.length && preset !== "vercel" && preset !== "vercel-edge") warnings.push(`${preset} does not map Farm per-route region hints.`);
2216
+ if (runtime.maxDuration && preset !== "vercel") warnings.push(`${preset} does not map Farm per-route maxDuration.`);
2217
+ return {
2218
+ pathname: normalizedPathname,
2219
+ pattern: page.pattern,
2220
+ params: page.params,
2221
+ filePath: toProjectPath(root, page.filePath),
2222
+ source: page.source,
2223
+ layouts: layouts.map((filePath) => toProjectPath(root, filePath)),
2224
+ middleware,
2225
+ runtime,
2226
+ rendering,
2227
+ cache,
2228
+ metadata: {
2229
+ static: metadataSources.filter(({ source }) => /export\s+const\s+metadata\b/.test(source)).map(({ filePath }) => toProjectPath(root, filePath)),
2230
+ dynamic: metadataSources.filter(({ source }) => /export\s+(?:async\s+)?function\s+generateMetadata\b|export\s+const\s+generateMetadata\b/.test(source)).map(({ filePath }) => toProjectPath(root, filePath)),
2231
+ ...openGraphImage ? { openGraphImage: toProjectPath(root, openGraphImage) } : {},
2232
+ ...twitterImage ? { twitterImage: toProjectPath(root, twitterImage) } : {}
2233
+ },
2234
+ deployment: {
2235
+ target: String(config.deploy.target || "node"),
2236
+ preset,
2237
+ runtime: presetRuntime,
2238
+ compatible,
2239
+ warnings
2240
+ }
2241
+ };
2242
+ }
2243
+ function formatFarmRouteExplanation(explanation, options = {}) {
2244
+ const color = options.color === void 0 ? pc : pc.createColors(options.color);
2245
+ const lines = [
2246
+ color.bold("FARM / EXPLAIN"),
2247
+ "",
2248
+ `${color.bold("Path")} ${explanation.pathname}`,
2249
+ `${color.bold("Pattern")} ${explanation.pattern}`,
2250
+ `${color.bold("File")} ${explanation.filePath}`,
2251
+ `${color.bold("Source")} ${explanation.source}`,
2252
+ `${color.bold("Params")} ${formatParams(explanation.params)}`,
2253
+ `${color.bold("Layouts")} ${explanation.layouts.length ? explanation.layouts.join(" -> ") : "none"}`,
2254
+ `${color.bold("Middleware")} ${explanation.middleware.length ? explanation.middleware.map((entry) => entry.filePath).join(", ") : "none"}`,
2255
+ `${color.bold("Runtime")} ${formatRuntime(explanation.runtime)}`,
2256
+ `${color.bold("Rendering")} ${explanation.rendering.mode} (${explanation.rendering.reason})${explanation.rendering.ppr ? ", PPR" : ""}`,
2257
+ `${color.bold("Caching")} ${formatCaching(explanation.cache)}`,
2258
+ `${color.bold("Metadata")} ${formatMetadata(explanation.metadata)}`,
2259
+ `${color.bold("Deployment")} ${explanation.deployment.target} / ${explanation.deployment.preset} — ${explanation.deployment.compatible ? color.green("compatible") : color.red("incompatible")}`
2260
+ ];
2261
+ for (const warning of explanation.deployment.warnings) lines.push(` ${color.yellow("!")} ${warning}`);
2262
+ return lines.join("\n");
2263
+ }
2264
+ function discoverMatchingPages(config, pathname) {
2265
+ const candidates = [];
2266
+ for (const [priority, source] of getFarmSourceRoots(config).entries()) {
2267
+ const appDirectory = path.join(source.root, source.srcDir, "app");
2268
+ if (existsSync(appDirectory)) for (const filePath of walkFiles(appDirectory)) {
2269
+ if (!/^page\.(?:tsx?|jsx?|vue|svelte|mdx?)$/.test(path.basename(filePath))) continue;
2270
+ const relativeDirectory = path.relative(appDirectory, path.dirname(filePath));
2271
+ if (relativeDirectory.split(path.sep).includes("api") || isRouteSlotDirectory(relativeDirectory)) continue;
2272
+ const pattern = directoryToRoutePattern(relativeDirectory);
2273
+ const match = matchRoutePattern(pattern, pathname);
2274
+ if (!match) continue;
2275
+ candidates.push({
2276
+ filePath,
2277
+ pattern,
2278
+ params: match.params,
2279
+ score: match.score,
2280
+ source: source.name,
2281
+ priority
2282
+ });
2283
+ }
2284
+ const sourceDirectory = path.join(source.root, source.srcDir);
2285
+ if (!existsSync(sourceDirectory)) continue;
2286
+ for (const filePath of walkFiles(sourceDirectory)) {
2287
+ if (!/\.(?:tsx?|jsx?)$/.test(filePath) || filePath.endsWith(".d.ts")) continue;
2288
+ const moduleSource = readFileSync(filePath, "utf8");
2289
+ for (const pattern of scanProgrammaticPagePaths(moduleSource)) {
2290
+ const match = matchRoutePattern(pattern, pathname);
2291
+ if (!match) continue;
2292
+ candidates.push({
2293
+ filePath,
2294
+ pattern,
2295
+ params: match.params,
2296
+ score: match.score,
2297
+ source: source.name,
2298
+ priority
2299
+ });
2300
+ }
2301
+ }
2302
+ }
2303
+ return candidates;
2304
+ }
2305
+ function isRouteSlotDirectory(relativeDirectory) {
2306
+ return relativeDirectory.split(path.sep).some((segment) => /^@[A-Za-z][\w-]*$/.test(segment));
2307
+ }
2308
+ function walkFiles(directory) {
2309
+ const files = [];
2310
+ const pending = [directory];
2311
+ while (pending.length) {
2312
+ const current = pending.pop();
2313
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
2314
+ const entryPath = path.join(current, entry.name);
2315
+ if (entry.isDirectory()) {
2316
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
2317
+ pending.push(entryPath);
2318
+ continue;
2319
+ }
2320
+ files.push(entryPath);
2321
+ }
2322
+ }
2323
+ return files;
2324
+ }
2325
+ function directoryToRoutePattern(relativeDirectory) {
2326
+ const segments = relativeDirectory.split(path.sep).filter(Boolean).filter((segment) => !/^\(.+\)$/.test(segment) && !segment.startsWith("@")).map((segment) => segment.replace(/^\(\.{1,3}\)/, ""));
2327
+ return segments.length ? `/${segments.join("/")}` : "/";
2328
+ }
2329
+ function matchRoutePattern(pattern, pathname) {
2330
+ const patternSegments = splitPath(pattern);
2331
+ const pathSegments = splitPath(pathname);
2332
+ const params = {};
2333
+ let score = 0;
2334
+ let pathIndex = 0;
2335
+ for (const segment of patternSegments) {
2336
+ const optionalCatchAll = segment.match(/^\[\[\.\.\.(.+)\]\]$/);
2337
+ if (optionalCatchAll) {
2338
+ params[optionalCatchAll[1]] = pathSegments.slice(pathIndex);
2339
+ pathIndex = pathSegments.length;
2340
+ score += 1;
2341
+ continue;
2342
+ }
2343
+ const catchAll = segment.match(/^\[\.\.\.(.+)\]$/);
2344
+ if (catchAll) {
2345
+ if (pathIndex >= pathSegments.length) return null;
2346
+ params[catchAll[1]] = pathSegments.slice(pathIndex);
2347
+ pathIndex = pathSegments.length;
2348
+ score += 10;
2349
+ continue;
2350
+ }
2351
+ const dynamic = segment.match(/^\[(.+)\]$/);
2352
+ if (dynamic) {
2353
+ if (pathIndex >= pathSegments.length) return null;
2354
+ params[dynamic[1]] = pathSegments[pathIndex++];
2355
+ score += 50;
2356
+ continue;
2357
+ }
2358
+ if (segment !== pathSegments[pathIndex++]) return null;
2359
+ score += 100;
2360
+ }
2361
+ return pathIndex === pathSegments.length ? {
2362
+ params,
2363
+ score
2364
+ } : null;
2365
+ }
2366
+ function collectInheritedRouteFiles(config, pathname, baseName, extensions) {
2367
+ return collectLayeredRouteFiles(config, baseName, extensions).filter((entry) => matchesRoutePrefix(entry.pattern, pathname)).sort(compareInheritedRouteFiles).map((entry) => entry.filePath);
2368
+ }
2369
+ function collectMiddleware(root, hasConfigMiddleware, config, pathname) {
2370
+ const rootMiddleware = /* @__PURE__ */ new Map();
2371
+ for (const source of getFarmSourceRoots(config)) {
2372
+ const filePath = findFile(path.join(source.root, source.srcDir), "middleware", MIDDLEWARE_EXTENSIONS);
2373
+ if (filePath) rootMiddleware.set("root", {
2374
+ filePath,
2375
+ pattern: "/"
2376
+ });
2377
+ }
2378
+ const files = [...rootMiddleware.values(), ...collectLayeredRouteFiles(config, "middleware", MIDDLEWARE_EXTENSIONS).filter((entry) => matchesRoutePrefix(entry.pattern, pathname))].sort(compareInheritedRouteFiles).map((entry) => entry.filePath);
2379
+ return [...hasConfigMiddleware ? [{
2380
+ source: "config",
2381
+ filePath: "farm.config (middleware)"
2382
+ }] : [], ...files.map((filePath) => ({
2383
+ source: "file",
2384
+ filePath: toProjectPath(root, filePath)
2385
+ }))];
2386
+ }
2387
+ function collectLayeredRouteFiles(config, baseName, extensions) {
2388
+ const files = /* @__PURE__ */ new Map();
2389
+ const filePattern = new RegExp(`^${escapeRegExp(baseName)}\\.(?:${extensions.map(escapeRegExp).join("|")})$`);
2390
+ for (const source of getFarmSourceRoots(config)) {
2391
+ const appDirectory = path.join(source.root, source.srcDir, "app");
2392
+ if (!existsSync(appDirectory)) continue;
2393
+ for (const filePath of walkFiles(appDirectory)) {
2394
+ if (!filePattern.test(path.basename(filePath))) continue;
2395
+ const pattern = directoryToRoutePattern(path.relative(appDirectory, path.dirname(filePath)));
2396
+ files.set(pattern, {
2397
+ filePath,
2398
+ pattern
2399
+ });
2400
+ }
2401
+ }
2402
+ return [...files.values()];
2403
+ }
2404
+ function findNearestSocialImage(config, pathname, baseName) {
2405
+ return collectLayeredRouteFiles(config, baseName, SOCIAL_IMAGE_EXTENSIONS).filter((entry) => matchesRoutePrefix(entry.pattern, pathname)).sort((left, right) => compareInheritedRouteFiles(right, left))[0]?.filePath;
2406
+ }
2407
+ function compareInheritedRouteFiles(left, right) {
2408
+ return splitPath(left.pattern).length - splitPath(right.pattern).length;
2409
+ }
2410
+ function matchesRoutePrefix(pattern, pathname) {
2411
+ const patternSegments = splitPath(pattern);
2412
+ const pathSegments = splitPath(pathname);
2413
+ if (patternSegments.length > pathSegments.length) return false;
2414
+ return patternSegments.every((segment, index) => {
2415
+ if (/^\[{1,2}(?:\.\.\.)?.+\]{1,2}$/.test(segment)) return true;
2416
+ return segment === pathSegments[index];
2417
+ });
2418
+ }
2419
+ function findFile(directory, baseName, extensions) {
2420
+ return extensions.map((extension) => path.join(directory, `${baseName}.${extension}`)).find(existsSync);
2421
+ }
2422
+ function escapeRegExp(value) {
2423
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2424
+ }
2425
+ function readRuntimeExports(source) {
2426
+ const runtime = readStringExport(source, "runtime");
2427
+ const maxDuration = readNumberOrAutoExport(source, "maxDuration");
2428
+ const regions = readStringArrayOrAutoExport(source, "regions");
2429
+ return {
2430
+ ...runtime === "auto" || runtime === "node" || runtime === "edge" ? { runtime } : {},
2431
+ ...regions ? { regions } : {},
2432
+ ...maxDuration !== void 0 ? { maxDuration } : {}
2433
+ };
2434
+ }
2435
+ function resolveRendering(pageSource, matchingRules) {
2436
+ const pageRendering = resolveRouteRenderingConfig({
2437
+ ...readBooleanExport(pageSource, "ssg") !== void 0 ? { ssg: readBooleanExport(pageSource, "ssg") } : {},
2438
+ ...readBooleanExport(pageSource, "ppr") !== void 0 ? { ppr: readBooleanExport(pageSource, "ppr") } : {},
2439
+ ...readBooleanExport(pageSource, "experimental_ppr") !== void 0 ? { experimental_ppr: readBooleanExport(pageSource, "experimental_ppr") } : {},
2440
+ ...readNumberOrFalseExport(pageSource, "revalidate") !== void 0 ? { revalidate: readNumberOrFalseExport(pageSource, "revalidate") } : {},
2441
+ ...readDynamicExport(pageSource) ? { dynamic: readDynamicExport(pageSource) } : {}
2442
+ }, pageSource);
2443
+ let mode = pageRendering.ssg ? "static" : pageRendering.ppr ? "partial" : "dynamic";
2444
+ let reason = pageRendering.directive ? `page directive ${JSON.stringify(pageRendering.directive)}` : pageRendering.ssg ? "page static rendering declaration" : pageRendering.ppr ? "page PPR declaration" : "default server rendering";
2445
+ let ppr = pageRendering.ppr;
2446
+ for (const [pattern, rule] of matchingRules) if (rule.prerender === true || rule.render === "static") {
2447
+ mode = "static";
2448
+ reason = `routeRules ${pattern}`;
2449
+ ppr = false;
2450
+ } else if (rule.prerender === false || rule.render === "dynamic" || rule.ssr === true) {
2451
+ mode = "dynamic";
2452
+ reason = `routeRules ${pattern}`;
2453
+ ppr = false;
2454
+ } else if (rule.ssr === false) {
2455
+ mode = "client";
2456
+ reason = `routeRules ${pattern}`;
2457
+ ppr = false;
2458
+ }
2459
+ return {
2460
+ mode,
2461
+ reason,
2462
+ ppr
2463
+ };
2464
+ }
2465
+ function resolveCaching(pageSource, matchingRules) {
2466
+ let swr;
2467
+ let isr;
2468
+ for (const [, rule] of matchingRules) {
2469
+ if (typeof rule.swr === "number" || typeof rule.swr === "boolean") swr = rule.swr;
2470
+ if (typeof rule.isr === "number" || typeof rule.isr === "boolean") isr = rule.isr;
2471
+ }
2472
+ return {
2473
+ ...readNumberOrFalseExport(pageSource, "revalidate") !== void 0 ? { revalidate: readNumberOrFalseExport(pageSource, "revalidate") } : {},
2474
+ ...swr !== void 0 ? { swr } : {},
2475
+ ...isr !== void 0 ? { isr } : {},
2476
+ rules: matchingRules.map(([pattern]) => pattern)
2477
+ };
2478
+ }
2479
+ function readStringExport(source, name) {
2480
+ return source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*["']([^"']+)["']`))?.[1];
2481
+ }
2482
+ function readDynamicExport(source) {
2483
+ const dynamic = readStringExport(source, "dynamic");
2484
+ return dynamic === "auto" || dynamic === "force-dynamic" || dynamic === "error" || dynamic === "force-static" ? dynamic : void 0;
2485
+ }
2486
+ function readBooleanExport(source, name) {
2487
+ const value = source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*(true|false)`))?.[1];
2488
+ return value === void 0 ? void 0 : value === "true";
2489
+ }
2490
+ function readNumberOrAutoExport(source, name) {
2491
+ const value = source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*(?:["'](auto)["']|(\\d+))`));
2492
+ return value?.[1] === "auto" ? "auto" : value?.[2] ? Number(value[2]) : void 0;
2493
+ }
2494
+ function readNumberOrFalseExport(source, name) {
2495
+ const value = source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*(false|\\d+)`))?.[1];
2496
+ return value === "false" ? false : value ? Number(value) : void 0;
2497
+ }
2498
+ function readStringArrayOrAutoExport(source, name) {
2499
+ if (source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*["']auto["']`))) return "auto";
2500
+ const array = source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*\\[([^\\]]*)\\]`))?.[1];
2501
+ if (array === void 0) return void 0;
2502
+ return [...array.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
2503
+ }
2504
+ function routeSpecificity(pattern) {
2505
+ return splitPath(pattern).reduce((score, segment) => {
2506
+ if (segment === "**" || segment.startsWith("[[...")) return score + 1;
2507
+ if (segment === "*" || segment.startsWith("[...")) return score + 10;
2508
+ if (segment.startsWith("[") || segment.startsWith(":")) return score + 50;
2509
+ return score + 100;
2510
+ }, 0);
2511
+ }
2512
+ function normalizePathname(value, basePath) {
2513
+ let pathname;
2514
+ try {
2515
+ pathname = new URL(value, "http://farm.local").pathname;
2516
+ } catch {
2517
+ pathname = value;
2518
+ }
2519
+ pathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
2520
+ const normalizedBase = basePath && basePath !== "/" ? `/${basePath.replace(/^\/+|\/+$/g, "")}` : "";
2521
+ if (normalizedBase && (pathname === normalizedBase || pathname.startsWith(`${normalizedBase}/`))) pathname = pathname.slice(normalizedBase.length) || "/";
2522
+ return pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
2523
+ }
2524
+ function splitPath(value) {
2525
+ return value.split("/").filter(Boolean).map(decodeURIComponent);
2526
+ }
2527
+ function toProjectPath(root, filePath) {
2528
+ let relativePath = path.relative(root, filePath);
2529
+ if ((relativePath === ".." || relativePath.startsWith(`..${path.sep}`)) && existsSync(root) && existsSync(filePath)) relativePath = path.relative(realpathSync(root), realpathSync(filePath));
2530
+ return relativePath.split(path.sep).join("/");
2531
+ }
2532
+ function formatParams(params) {
2533
+ const entries = Object.entries(params);
2534
+ return entries.length ? entries.map(([key, value]) => `${key}=${Array.isArray(value) ? value.join("/") : value}`).join(", ") : "none";
2535
+ }
2536
+ function formatRuntime(runtime) {
2537
+ return [
2538
+ runtime.runtime,
2539
+ runtime.regions?.length ? `regions=${runtime.regions.join(",")}` : "",
2540
+ runtime.maxDuration ? `maxDuration=${runtime.maxDuration}s` : ""
2541
+ ].filter(Boolean).join(", ");
2542
+ }
2543
+ function formatCaching(cache) {
2544
+ const values = [
2545
+ cache.revalidate !== void 0 ? `revalidate=${cache.revalidate}` : "",
2546
+ cache.swr !== void 0 ? `swr=${cache.swr}` : "",
2547
+ cache.isr !== void 0 ? `isr=${cache.isr}` : "",
2548
+ cache.rules.length ? `rules=${cache.rules.join(",")}` : ""
2549
+ ].filter(Boolean);
2550
+ return values.length ? values.join("; ") : "request-time / no declared cache";
2551
+ }
2552
+ function formatMetadata(metadata) {
2553
+ const values = [
2554
+ metadata.static.length ? `static=${metadata.static.join(",")}` : "",
2555
+ metadata.dynamic.length ? `dynamic=${metadata.dynamic.join(",")}` : "",
2556
+ metadata.openGraphImage ? `og=${metadata.openGraphImage}` : "",
2557
+ metadata.twitterImage ? `twitter=${metadata.twitterImage}` : ""
2558
+ ].filter(Boolean);
2559
+ return values.length ? values.join("; ") : "none";
2560
+ }
2561
+ //#endregion
1802
2562
  //#region src/cron.ts
1803
2563
  async function loadFarmCronConfig(options = {}) {
1804
2564
  const root = path.resolve(options.root || process.cwd());
@@ -2004,7 +2764,7 @@ function resolveMigrationCommand(root, entry, index) {
2004
2764
  }
2005
2765
  function runMigrationCommand(command) {
2006
2766
  return new Promise((resolve, reject) => {
2007
- const child = spawn(command.command, {
2767
+ const child = spawn$1(command.command, {
2008
2768
  cwd: command.cwd,
2009
2769
  env: {
2010
2770
  ...process.env,
@@ -2573,11 +3333,13 @@ function getDependencySectionFlags(packageManager, section) {
2573
3333
  }
2574
3334
  function runFarmUpgradeCommand(command) {
2575
3335
  return new Promise((resolve, reject) => {
2576
- const executable = process.platform === "win32" ? `${command.command}.cmd` : command.command;
2577
- const child = spawn(executable, command.args, {
3336
+ const isWindows = process.platform === "win32";
3337
+ const executable = isWindows ? `${command.command}.cmd` : command.command;
3338
+ const child = spawn$1(executable, command.args, {
2578
3339
  cwd: command.cwd,
2579
3340
  env: process.env,
2580
- stdio: "inherit"
3341
+ stdio: "inherit",
3342
+ shell: isWindows
2581
3343
  });
2582
3344
  child.on("error", reject);
2583
3345
  child.on("close", (code, signal) => {
@@ -2603,6 +3365,6 @@ function formatError(error) {
2603
3365
  return error instanceof Error ? error.message : String(error);
2604
3366
  }
2605
3367
  //#endregion
2606
- export { addFarmIntegration, buildFarm, createFarmUpgradePlan, createFrameworkMigrationPlan, createGatewaySession, createPreviewGatewayPlan, createPreviewTunnelPlan, createServer, deployFarm, detectFarmPackageManager, formatFarmCronJobs, formatFarmDoctorReport, formatFarmUpgradePlan, forwardGatewayRequest, generateFarmArtifacts, inspectFrameworkMigrations, listFarmCronJobs, listFarmIntegrationProviders, loadFarmCronConfig, migrateFarm, migrateFarmAuth, parsePreviewPublicUrl, previewFarm, resolveCloudflareAgentDeployPlan, resolvePreviewTarget, runFarmCronJob, runFarmDoctor, runPreviewGateway, startDevServer, startFarmCronScheduler, upgradeFarm };
3368
+ export { FarmDeployError, FarmGeneratedArtifactsStaleError, FarmStartError, addFarmIntegration, buildFarm, createFarmDeployPlan, createFarmStartPlan, createFarmUpgradePlan, createFrameworkMigrationPlan, createGatewaySession, createPreviewGatewayPlan, createPreviewTunnelPlan, createServer, deployFarm, detectFarmPackageManager, escapeDoubleQuoted, escapeSqlString, explainFarmRoute, flushFarmTelemetry, formatFarmCronJobs, formatFarmDeployPlan, formatFarmDoctorReport, formatFarmRouteExplanation, formatFarmUpgradePlan, forwardGatewayRequest, generateFarmArtifacts, getFarmTelemetryConfigFile, getFarmTelemetryStatus, inspectFrameworkMigrations, listFarmCronJobs, listFarmIntegrationProviders, loadFarmCronConfig, migrateFarm, migrateFarmAuth, parsePreviewPublicUrl, previewFarm, resolveCloudflareAgentDeployPlan, resolveFarmCreateAppTelemetryCommand, resolveFarmTelemetryCommand, resolvePreviewTarget, runFarmCronJob, runFarmDoctor, runNativePreviewTunnel, runPreviewGateway, setFarmTelemetryEnabled, showFarmTelemetryNotice, startDevServer, startFarm, startFarmCronScheduler, trackFarmCommand, trackFarmCreateAppCommand, trackFarmProjectCreated, upgradeFarm };
2607
3369
 
2608
3370
  //# sourceMappingURL=index.mjs.map