@farm.js/cli 0.1.0-beta.5 → 0.1.0-beta.50

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,52 +1,172 @@
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 { getFarmTelemetryConfigFile, getFarmTelemetryStatus, resolveFarmTelemetryCommand, setFarmTelemetryEnabled, showFarmTelemetryNotice, trackFarmCommand, trackFarmProjectCreated } from "./telemetry.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 } 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 { APITypeGenerator, generateEnvTypes, generateFarmI18nTypes, generateRouteTypes, getFarmDocsRouteTypeEntries, getFarmSourceRoots, getIntegrationSchemas, getPresetForDeployTarget, loadConfig, logger, normalizeDeployTarget, resolveConfig, resolveDeployConfig, resolveDeployOutputPath } from "@farm.js/core";
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";
12
13
  import { spawn, spawnSync } from "node:child_process";
13
14
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
14
15
  import pc from "picocolors";
15
16
  import { Cron } from "croner";
17
+ import { pathToFileURL } from "node:url";
16
18
  //#region \0rolldown/runtime.js
17
19
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
18
20
  //#endregion
19
21
  //#region src/deploy.ts
22
+ var FarmDeployError = class extends Error {
23
+ constructor(code, platform, message, options) {
24
+ super(message);
25
+ this.name = "FarmDeployError";
26
+ this.code = code;
27
+ this.platform = platform;
28
+ this.cause = options?.cause;
29
+ }
30
+ };
20
31
  /**
21
32
  * Deploy Farm.js application
22
33
  */
23
34
  async function deployFarm(options = {}) {
24
- const root = options.root || process.cwd();
35
+ const { plan, deployConfig } = await resolveFarmDeployContext(options);
36
+ if (options.plan) {
37
+ logger.info(formatFarmDeployPlan(plan));
38
+ return plan;
39
+ }
40
+ logger.info(`🚀 Building with ${plan.preset} preset...`);
41
+ await buildFarm({
42
+ root: plan.root,
43
+ preset: plan.preset,
44
+ target: plan.target
45
+ });
46
+ 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.`);
47
+ await deployPlatform(plan.target, plan.root, plan.outputDir, deployConfig, options.prod);
48
+ return plan;
49
+ }
50
+ async function createFarmDeployPlan(options = {}) {
51
+ return (await resolveFarmDeployContext(options)).plan;
52
+ }
53
+ function formatFarmDeployPlan(plan) {
54
+ return [
55
+ "FARM / DEPLOY PLAN",
56
+ "",
57
+ `Target: ${plan.target}`,
58
+ `Preset: ${plan.preset}`,
59
+ `Runtime: ${plan.runtime}`,
60
+ `Output: ${plan.outputDir}`,
61
+ `Production: ${plan.production ? "yes" : "no"}`,
62
+ "",
63
+ `1. ${plan.build.command}`,
64
+ ` cwd: ${plan.build.cwd}`,
65
+ `2. ${plan.deploy.command}`,
66
+ ` cwd: ${plan.deploy.cwd}`,
67
+ ...plan.cloudflareAgent ? ["", `Cloudflare Agent config: ${plan.cloudflareAgent.configPath}${plan.cloudflareAgent.generated ? " (generated during build)" : ""}`] : []
68
+ ].join("\n");
69
+ }
70
+ async function resolveFarmDeployContext(options) {
71
+ const root = path$1.resolve(options.root || process.cwd());
25
72
  const mode = "production";
26
73
  const userConfig = await loadConfig(root, void 0, mode);
27
- const config = userConfig ? await resolveConfig(userConfig, mode) : void 0;
74
+ const config = await resolveConfig({
75
+ root,
76
+ ...userConfig
77
+ }, mode);
28
78
  const cliTarget = options.vercel ? "vercel" : options.cloudflare ? "cloudflare" : options.netlify ? "netlify" : void 0;
29
- const platform = normalizeDeployTarget(cliTarget || config?.deploy.target);
30
- if (platform !== "vercel" && platform !== "cloudflare" && platform !== "netlify") {
31
- logger.error("Please specify a deployment target with --vercel, --cloudflare, --netlify, or farm.config deploy.target.");
32
- process.exit(1);
33
- }
79
+ const platform = normalizeDeployTarget(cliTarget || config.deploy.target);
80
+ if (platform !== "vercel" && platform !== "cloudflare" && platform !== "netlify") throw new Error("Please specify a deployment target with --vercel, --cloudflare, --netlify, or farm.config deploy.target.");
81
+ const configuredPreset = userConfig?.deploy?.preset || userConfig?.preset;
82
+ const configuredPresetTarget = getDeployTargetForPreset(configuredPreset);
83
+ const configuredTarget = normalizeDeployTarget(userConfig?.deploy?.target);
84
+ const presetMatchesPlatform = Boolean(configuredPreset) && (configuredPresetTarget === platform || !configuredPresetTarget && configuredTarget === platform);
85
+ if (cliTarget && configuredPreset && !presetMatchesPlatform) logger.warn(`Configured preset "${configuredPreset}" targets ${configuredPresetTarget || "an unknown platform"}; using the "${getPresetForDeployTarget(platform)}" preset because --${cliTarget} was passed.`);
34
86
  const deployConfig = resolveDeployConfig(userConfig || {}, {
35
87
  target: platform,
36
- preset: cliTarget ? userConfig?.deploy?.preset || userConfig?.preset || getPresetForDeployTarget(platform) : void 0
88
+ preset: cliTarget ? presetMatchesPlatform ? configuredPreset : getPresetForDeployTarget(platform) : void 0
37
89
  });
38
90
  const preset = deployConfig.preset || getPresetForDeployTarget(platform) || "node-server";
39
- logger.info(`🚀 Building with ${preset} preset...`);
40
- await buildFarm({
41
- root,
42
- preset
43
- });
44
91
  const nitroOutput = resolveDeployOutputPath(root, deployConfig.outputDir);
45
- if (!existsSync$1(nitroOutput)) {
46
- logger.error(`Build output not found at ${nitroOutput}. Please run 'farm build' first.`);
47
- process.exit(1);
92
+ const cloudflareAgent = platform === "cloudflare" ? resolveCloudflareAgentDeployPlan(root) || resolveConfiguredCloudflareAgentDeployPlan(root, config.integrations) : void 0;
93
+ const deploy = createDeployCommand(platform, root, nitroOutput, deployConfig, options.prod, cloudflareAgent);
94
+ return {
95
+ plan: {
96
+ root: path$1.resolve(root),
97
+ target: platform,
98
+ preset,
99
+ runtime: getFarmPresetRuntime(preset),
100
+ outputDir: nitroOutput,
101
+ production: platform === "netlify" || Boolean(options.prod),
102
+ build: {
103
+ command: `farm build --preset ${preset}`,
104
+ cwd: path$1.resolve(root)
105
+ },
106
+ deploy,
107
+ ...cloudflareAgent ? { cloudflareAgent } : {}
108
+ },
109
+ deployConfig
110
+ };
111
+ }
112
+ function createDeployCommand(platform, root, outputDir, deployConfig, prod, cloudflareAgent) {
113
+ if (platform === "vercel") {
114
+ const args = [
115
+ "deploy",
116
+ "--prebuilt",
117
+ "--yes",
118
+ ...prod ? ["--prod"] : []
119
+ ];
120
+ return {
121
+ command: formatCommand$1("vercel", args),
122
+ cwd: path$1.resolve(root),
123
+ executable: "vercel",
124
+ args
125
+ };
126
+ }
127
+ if (platform === "netlify") {
128
+ const args = createNetlifyDeployArgs(deployConfig.netlify?.site);
129
+ return {
130
+ command: formatCommand$1("netlify", args),
131
+ cwd: outputDir,
132
+ executable: "netlify",
133
+ args
134
+ };
135
+ }
136
+ if (cloudflareAgent) {
137
+ const args = [
138
+ "deploy",
139
+ "--config",
140
+ cloudflareAgent.configPath,
141
+ ...cloudflareAgent.environment ? ["--env", cloudflareAgent.environment] : []
142
+ ];
143
+ return {
144
+ command: formatCommand$1("wrangler", args),
145
+ cwd: path$1.resolve(root),
146
+ executable: "wrangler",
147
+ args
148
+ };
48
149
  }
49
- await deployPlatform(platform, root, nitroOutput, deployConfig, options.prod);
150
+ const args = [
151
+ "pages",
152
+ "deploy",
153
+ ".",
154
+ `--project-name=${deployConfig.cloudflare?.projectName || deployConfig.projectName || "farm-app"}`
155
+ ];
156
+ return {
157
+ command: formatCommand$1("wrangler", args),
158
+ cwd: outputDir,
159
+ executable: "wrangler",
160
+ args
161
+ };
162
+ }
163
+ function createNetlifyDeployArgs(site) {
164
+ return [
165
+ "deploy",
166
+ "--prod",
167
+ "--dir=.",
168
+ ...site ? [`--site=${site}`] : []
169
+ ];
50
170
  }
51
171
  /**
52
172
  * Deploy using platform's native CLI (user credentials)
@@ -68,19 +188,20 @@ async function deployPlatform(platform, root, outputDir, deployConfig, prod) {
68
188
  async function deployVercel(root, outputDir, prod) {
69
189
  logger.info("🚀 Deploying to Vercel...");
70
190
  try {
71
- execSync("vercel --version", { stdio: "ignore" });
72
- } catch {
73
- logger.error("❌ Vercel CLI is not installed.");
74
- logger.info("💡 Install it with: npm i -g vercel");
75
- process.exit(1);
191
+ execFileSync("vercel", ["--version"], {
192
+ stdio: "ignore",
193
+ cwd: root
194
+ });
195
+ } catch (error) {
196
+ throw new FarmDeployError("CLI_NOT_INSTALLED", "vercel", "Vercel CLI is not installed. Install it with: npm i -g vercel", { cause: error });
76
197
  }
77
198
  try {
78
- execSync("vercel whoami", { stdio: "ignore" });
79
- } catch {
80
- logger.warn("⚠️ Not logged in to Vercel.");
81
- logger.info("💡 Please run: vercel login");
82
- logger.info(" Then run: farm deploy --vercel");
83
- process.exit(1);
199
+ execFileSync("vercel", ["whoami"], {
200
+ stdio: "ignore",
201
+ cwd: root
202
+ });
203
+ } catch (error) {
204
+ throw new FarmDeployError("CLI_NOT_AUTHENTICATED", "vercel", "Vercel CLI is not authenticated. Run 'vercel login', then retry the deployment.", { cause: error });
84
205
  }
85
206
  try {
86
207
  const { existsSync, statSync, readdirSync } = await import("fs");
@@ -89,14 +210,8 @@ async function deployVercel(root, outputDir, prod) {
89
210
  const configFile = path$1.join(outputDir, "config.json");
90
211
  const serverIndex = path$1.join(functionsDir, "index.mjs");
91
212
  logger.info("🔍 Verifying deployment structure...");
92
- if (!existsSync(functionsDir)) {
93
- logger.error(`❌ Functions directory not found at ${functionsDir}`);
94
- process.exit(1);
95
- }
96
- if (!existsSync(serverIndex)) {
97
- logger.error(`❌ Server entry point not found at ${serverIndex}`);
98
- process.exit(1);
99
- }
213
+ if (!existsSync(functionsDir)) throw new FarmDeployError("INVALID_BUILD_OUTPUT", "vercel", `Functions directory not found at ${functionsDir}.`);
214
+ if (!existsSync(serverIndex)) throw new FarmDeployError("INVALID_BUILD_OUTPUT", "vercel", `Server entry point not found at ${serverIndex}.`);
100
215
  logger.info(`✅ Functions directory: ${functionsDir}`);
101
216
  if (!existsSync(staticDir)) logger.warn(`⚠️ Static directory not found at ${staticDir}`);
102
217
  else {
@@ -177,14 +292,19 @@ async function deployVercel(root, outputDir, prod) {
177
292
  logger.info(` Static: ${staticDir}`);
178
293
  logger.info(` Config: ${configFile}`);
179
294
  logger.info("📤 Uploading to Vercel...");
180
- execSync(`vercel deploy --prebuilt --yes${prod ? " --prod" : ""}`, {
295
+ execFileSync("vercel", [
296
+ "deploy",
297
+ "--prebuilt",
298
+ "--yes",
299
+ ...prod ? ["--prod"] : []
300
+ ], {
181
301
  stdio: "inherit",
182
302
  cwd: root
183
303
  });
184
304
  logger.success("✅ Deployed to Vercel successfully!");
185
305
  } catch (error) {
186
- logger.error(`❌ Failed to deploy to Vercel: ${error.message}`);
187
- process.exit(1);
306
+ if (error instanceof FarmDeployError) throw error;
307
+ throw new FarmDeployError("DEPLOY_FAILED", "vercel", `Failed to deploy to Vercel: ${getErrorMessage(error)}`, { cause: error });
188
308
  }
189
309
  }
190
310
  /** Deploy to a composed Worker or fall back to Cloudflare Pages. */
@@ -205,8 +325,7 @@ async function deployCloudflare(root, outputDir, projectName) {
205
325
  });
206
326
  logger.success("✅ Deployed Farm and Cloudflare Agents successfully!");
207
327
  } catch (error) {
208
- logger.error(`❌ Failed to deploy to Cloudflare: ${error.message}`);
209
- process.exit(1);
328
+ throw new FarmDeployError("DEPLOY_FAILED", "cloudflare", `Failed to deploy to Cloudflare: ${getErrorMessage(error)}`, { cause: error });
210
329
  }
211
330
  return;
212
331
  }
@@ -224,8 +343,7 @@ async function deployCloudflare(root, outputDir, projectName) {
224
343
  });
225
344
  logger.success("✅ Deployed to Cloudflare Pages successfully!");
226
345
  } catch (error) {
227
- logger.error(`❌ Failed to deploy to Cloudflare: ${error.message}`);
228
- process.exit(1);
346
+ throw new FarmDeployError("DEPLOY_FAILED", "cloudflare", `Failed to deploy to Cloudflare: ${getErrorMessage(error)}`, { cause: error });
229
347
  }
230
348
  }
231
349
  /** Read the trusted Workers deployment handoff emitted by @farm.js/cf-agent. */
@@ -251,16 +369,34 @@ function resolveCloudflareAgentDeployPlan(root) {
251
369
  ...typeof environment === "string" ? { environment: environment.trim() } : {}
252
370
  };
253
371
  }
372
+ function resolveConfiguredCloudflareAgentDeployPlan(root, integrations) {
373
+ const integration = Object.values(integrations || {}).find((value) => isRecord(value) && value.category === "agent" && value.type === "cloudflare" && value.serverRuntime === false);
374
+ if (!isRecord(integration) || !isRecord(integration.instance)) return void 0;
375
+ const configuredPath = integration.instance.config;
376
+ if (typeof configuredPath !== "string" || !configuredPath.trim()) return void 0;
377
+ const projectRoot = path$1.resolve(root);
378
+ const sourceConfigPath = path$1.resolve(projectRoot, configuredPath);
379
+ assertPathInsideProject(projectRoot, sourceConfigPath, "Cloudflare Agents source config");
380
+ const configPath = path$1.join(path$1.dirname(sourceConfigPath), ".farm-cf-agent.wrangler.jsonc");
381
+ const environment = integration.instance.environment;
382
+ return {
383
+ configPath,
384
+ ...typeof environment === "string" && environment.trim() ? { environment: environment.trim() } : {},
385
+ generated: true
386
+ };
387
+ }
388
+ function assertPathInsideProject(projectRoot, candidate, label) {
389
+ const relativePath = path$1.relative(projectRoot, candidate);
390
+ if (relativePath === ".." || relativePath.startsWith(`..${path$1.sep}`) || path$1.isAbsolute(relativePath)) throw new Error(`${label} must stay inside the Farm project root.`);
391
+ }
254
392
  function assertWranglerInstalled(root) {
255
393
  try {
256
394
  execFileSync("wrangler", ["--version"], {
257
395
  stdio: "ignore",
258
396
  cwd: root
259
397
  });
260
- } catch {
261
- logger.error(" Wrangler CLI is not installed.");
262
- logger.info("💡 Install it in this project with: npm i -D wrangler");
263
- process.exit(1);
398
+ } catch (error) {
399
+ throw new FarmDeployError("CLI_NOT_INSTALLED", "cloudflare", "Wrangler CLI is not installed. Install it in this project with: npm i -D wrangler", { cause: error });
264
400
  }
265
401
  }
266
402
  function isRecord(value) {
@@ -272,22 +408,33 @@ function isRecord(value) {
272
408
  async function deployNetlify(root, outputDir, site) {
273
409
  logger.info("🚀 Deploying to Netlify...");
274
410
  try {
275
- execSync("netlify --version", { stdio: "ignore" });
276
- } catch {
277
- logger.error("❌ Netlify CLI is not installed.");
278
- logger.info("💡 Install it with: npm i -g netlify-cli");
279
- process.exit(1);
411
+ execFileSync("netlify", ["--version"], {
412
+ stdio: "ignore",
413
+ cwd: root
414
+ });
415
+ } catch (error) {
416
+ throw new FarmDeployError("CLI_NOT_INSTALLED", "netlify", "Netlify CLI is not installed. Install it with: npm i -g netlify-cli", { cause: error });
280
417
  }
281
418
  try {
282
- process.chdir(outputDir);
283
- const siteFlag = site ? ` --site=${site}` : "";
284
- execSync(`netlify deploy --prod --dir=.${siteFlag}`, { stdio: "inherit" });
419
+ execFileSync("netlify", createNetlifyDeployArgs(site), {
420
+ stdio: "inherit",
421
+ cwd: outputDir
422
+ });
285
423
  logger.success("✅ Deployed to Netlify successfully!");
286
424
  } catch (error) {
287
- logger.error(`❌ Failed to deploy to Netlify: ${error.message}`);
288
- process.exit(1);
425
+ throw new FarmDeployError("DEPLOY_FAILED", "netlify", `Failed to deploy to Netlify: ${getErrorMessage(error)}`, { cause: error });
289
426
  }
290
427
  }
428
+ function formatCommand$1(executable, args) {
429
+ return [executable, ...args].map(formatCommandArgument).join(" ");
430
+ }
431
+ function formatCommandArgument(argument) {
432
+ if (/^[A-Za-z0-9_./:=@+-]+$/.test(argument)) return argument;
433
+ return `'${argument.replace(/'/g, `'"'"'`)}'`;
434
+ }
435
+ function getErrorMessage(error) {
436
+ return error instanceof Error ? error.message : String(error);
437
+ }
291
438
  //#endregion
292
439
  //#region src/preview-gateway.ts
293
440
  const DEFAULT_GATEWAY_URL = "https://preview.farming-labs.dev";
@@ -310,10 +457,12 @@ const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
310
457
  function createPreviewGatewayPlan(target, options = {}) {
311
458
  const requestedName = sanitizePreviewName$1(options.name || process.env.FARM_PREVIEW_NAME) || randomPreviewName$1();
312
459
  const gatewayUrl = normalizeGatewayUrl(options.gatewayUrl || process.env.FARM_PREVIEW_GATEWAY_URL || DEFAULT_GATEWAY_URL);
460
+ const relayUrl = normalizeRelayUrl(process.env.FARM_PREVIEW_RELAY_URL || gatewayUrl);
313
461
  const requestedHostname = `${requestedName}.${normalizePreviewDomain$1(process.env.FARM_PREVIEW_DOMAIN || DEFAULT_PREVIEW_DOMAIN)}`;
314
462
  return {
315
463
  provider: "farm-gateway",
316
464
  gatewayUrl,
465
+ relayUrl,
317
466
  target,
318
467
  requestedName,
319
468
  requestedHostname,
@@ -478,6 +627,7 @@ async function closeGatewaySession(plan, session) {
478
627
  function formatGatewayPlan(plan) {
479
628
  return [
480
629
  `Gateway: ${plan.gatewayUrl}`,
630
+ `Relay: ${plan.relayUrl}`,
481
631
  `Local: ${plan.target.localUrl}`,
482
632
  `Public: ${plan.requestedPublicUrl}`
483
633
  ].join("\n");
@@ -489,6 +639,17 @@ function formatRequestPath(path) {
489
639
  function normalizeGatewayUrl(value) {
490
640
  return value.replace(/\/+$/, "");
491
641
  }
642
+ function normalizeRelayUrl(value) {
643
+ const url = new URL(value);
644
+ if (url.protocol === "http:") url.protocol = "ws:";
645
+ if (url.protocol === "https:") url.protocol = "wss:";
646
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new Error(`Preview relay must use ws or wss, received ${url.protocol}`);
647
+ const pathname = url.pathname.replace(/\/+$/, "");
648
+ url.pathname = pathname.endsWith("/agent") ? pathname : `${pathname}/agent`;
649
+ url.search = "";
650
+ url.hash = "";
651
+ return url.toString();
652
+ }
492
653
  function normalizePreviewDomain$1(value) {
493
654
  return value.replace(/^https?:\/\//, "").replace(/^\.*/, "").replace(/\/*$/, "");
494
655
  }
@@ -500,6 +661,39 @@ function randomPreviewName$1() {
500
661
  return `farm-${Math.random().toString(36).slice(2, 8)}`;
501
662
  }
502
663
  //#endregion
664
+ //#region src/preview-native.ts
665
+ async function runNativePreviewTunnel(plan, options = {}) {
666
+ const runtime = options.runtime || await loadNativeTunnel();
667
+ const session = await runtime.startPreviewAgent(plan.relayUrl, plan.requestedName, plan.target.localUrl);
668
+ let stopping = false;
669
+ const stop = () => {
670
+ if (stopping) return;
671
+ stopping = true;
672
+ runtime.stopPreviewAgent(session.sessionId);
673
+ };
674
+ process.once("SIGINT", stop);
675
+ process.once("SIGTERM", stop);
676
+ logger.success("Preview URL ready.");
677
+ logger.info(`Public: ${session.publicUrl}`);
678
+ logger.info("Forwarding requests through the native tunnel until Ctrl+C.");
679
+ try {
680
+ if (!await runtime.waitPreviewAgent(session.sessionId)) throw new Error("The native preview tunnel stopped before its lifecycle could be observed.");
681
+ return session;
682
+ } finally {
683
+ process.removeListener("SIGINT", stop);
684
+ process.removeListener("SIGTERM", stop);
685
+ await runtime.stopPreviewAgent(session.sessionId).catch(() => false);
686
+ }
687
+ }
688
+ async function loadNativeTunnel() {
689
+ try {
690
+ return await import("@farm.js/tunnel");
691
+ } catch (error) {
692
+ const reason = error instanceof Error ? ` ${error.message}` : "";
693
+ throw new Error(`Could not load @farm.js/tunnel for this platform. Reinstall @farm.js/cli so its native platform package is restored.${reason}`);
694
+ }
695
+ }
696
+ //#endregion
503
697
  //#region src/preview.ts
504
698
  const DEFAULT_PREVIEW_PORTS = [
505
699
  3e3,
@@ -523,15 +717,26 @@ async function previewFarm(options = {}) {
523
717
  plan
524
718
  };
525
719
  }
526
- logger.info(`Gateway: ${plan.gatewayUrl}`);
527
- logger.info("Opening Farm preview gateway session...");
528
- const session = await runPreviewGateway(plan, { timeoutMs: options.timeoutMs });
529
- return {
530
- target,
531
- plan,
532
- publicUrl: session.publicUrl,
533
- session
534
- };
720
+ logger.info(`Relay: ${plan.relayUrl}`);
721
+ logger.info("Opening native Farm preview tunnel...");
722
+ try {
723
+ const session = await runNativePreviewTunnel(plan);
724
+ return {
725
+ target,
726
+ plan,
727
+ publicUrl: session.publicUrl,
728
+ session
729
+ };
730
+ } catch (error) {
731
+ logger.warn(`Native preview relay unavailable; using compatibility gateway polling.${formatPreviewError(error)}`);
732
+ const session = await runPreviewGateway(plan, { timeoutMs: options.timeoutMs });
733
+ return {
734
+ target,
735
+ plan,
736
+ publicUrl: session.publicUrl,
737
+ session
738
+ };
739
+ }
535
740
  }
536
741
  const plan = createPreviewTunnelPlan(target, options);
537
742
  if (options.dryRun) {
@@ -549,6 +754,9 @@ async function previewFarm(options = {}) {
549
754
  publicUrl: await runPreviewTunnel(plan, options.timeoutMs ?? 3e4)
550
755
  };
551
756
  }
757
+ function formatPreviewError(error) {
758
+ return error instanceof Error && error.message ? ` ${error.message}` : "";
759
+ }
552
760
  function shouldUseManagedGateway(options) {
553
761
  if (options.provider === "farm" || process.env.FARM_PREVIEW_PROVIDER === "farm") return true;
554
762
  if (options.provider === "local" || process.env.FARM_PREVIEW_PROVIDER === "local") return false;
@@ -799,10 +1007,20 @@ function normalizePreviewDomain(value) {
799
1007
  }
800
1008
  //#endregion
801
1009
  //#region src/generate.ts
1010
+ var FarmGeneratedArtifactsStaleError = class extends Error {
1011
+ constructor(root, stalePaths) {
1012
+ const normalizedPaths = [...new Set(stalePaths)].sort();
1013
+ const relativePaths = normalizedPaths.map((filePath) => path.relative(root, filePath));
1014
+ super(`Generated types are stale:\n${relativePaths.map((filePath) => ` - ${filePath}`).join("\n")}\nRun farm generate and commit the updated files.`);
1015
+ this.name = "FarmGeneratedArtifactsStaleError";
1016
+ this.stalePaths = normalizedPaths;
1017
+ }
1018
+ };
802
1019
  const PRISMA_GENERATED_START = "// Farm.js integrations generated schema: start";
803
1020
  const PRISMA_GENERATED_END = "// Farm.js integrations generated schema: end";
804
1021
  async function generateFarmArtifacts(options = {}) {
805
1022
  const root = path.resolve(options.root || process.cwd());
1023
+ if (options.check && hasSchemaOptions(options)) throw new Error("--check verifies generated framework types and cannot be combined with schema output options.");
806
1024
  const userConfig = await loadConfig(root, options.configPath, "development");
807
1025
  if (!userConfig && hasSchemaOptions(options)) throw new Error("No Farm config found. Please create farm.config.ts or config.ts.");
808
1026
  const resolvedConfig = await resolveConfig({
@@ -810,34 +1028,28 @@ async function generateFarmArtifacts(options = {}) {
810
1028
  ...userConfig
811
1029
  }, "development");
812
1030
  const extraRoutes = [...resolvedConfig.openapi?.enabled && resolvedConfig.openapi.route ? [resolvedConfig.openapi.route] : [], ...getFarmDocsRouteTypeEntries(resolvedConfig.docs)];
813
- await generateRouteTypes({
1031
+ const typeArtifacts = await generateFarmTypeArtifacts({
814
1032
  root: resolvedConfig.root,
815
1033
  srcDir: resolvedConfig.srcDir,
1034
+ configPath: options.configPath,
1035
+ layers: resolvedConfig.layers,
816
1036
  extraRoutes,
817
- suppressLintOnLink: resolvedConfig.suppressLintOnLink
818
- });
819
- await generateEnvTypes({
820
- root: resolvedConfig.root,
821
- srcDir: resolvedConfig.srcDir,
822
- configPath: options.configPath
823
- });
824
- const appDir = path.join(resolvedConfig.root, resolvedConfig.srcDir, "app");
825
- const apiGenerator = new APITypeGenerator(appDir);
826
- const apiRoutes = apiGenerator.scanAPIRoutes();
827
- const apiTypesPath = path.join(resolvedConfig.root, resolvedConfig.srcDir, "lib", "api.generated.ts");
828
- await mkdir(path.dirname(apiTypesPath), { recursive: true });
829
- await writeFile(apiTypesPath, apiGenerator.generateAPIRouter(apiRoutes), "utf8");
830
- if (resolvedConfig.i18n.enabled) await generateFarmI18nTypes({
831
- root: resolvedConfig.root,
832
- srcDir: resolvedConfig.srcDir,
833
- config: resolvedConfig.i18n
1037
+ suppressLintOnLink: resolvedConfig.suppressLintOnLink,
1038
+ componentExtensions: resolvedConfig.renderer.componentExtensions,
1039
+ i18nConfig: resolvedConfig.i18n,
1040
+ check: options.check
834
1041
  });
835
- logger.success(`Generated route, API, env${resolvedConfig.i18n.enabled ? ", and i18n" : ""} types (${apiRoutes.length} API route${apiRoutes.length === 1 ? "" : "s"}).`);
1042
+ if (options.check) {
1043
+ if (typeArtifacts.stalePaths.length) throw new FarmGeneratedArtifactsStaleError(root, typeArtifacts.stalePaths);
1044
+ logger.success(`Generated route, API, env${resolvedConfig.i18n.enabled ? ", and i18n" : ""} types are up to date.`);
1045
+ return typeArtifacts;
1046
+ }
1047
+ logger.success(`Generated route, API, env${resolvedConfig.i18n.enabled ? ", and i18n" : ""} types (${typeArtifacts.apiRoutes.length} API route${typeArtifacts.apiRoutes.length === 1 ? "" : "s"}).`);
836
1048
  const schemas = getIntegrationSchemas(resolvedConfig.integrations);
837
1049
  const schemaEntries = Object.entries(schemas);
838
1050
  if (!schemaEntries.length) {
839
1051
  if (hasSchemaOptions(options)) logger.warn("No integration schemas were found in the current Farm config.");
840
- return;
1052
+ return typeArtifacts;
841
1053
  }
842
1054
  const packageManifest = await readPackageManifest(root);
843
1055
  const schemaOptionsExplicit = hasSchemaOptions(options);
@@ -847,12 +1059,12 @@ async function generateFarmArtifacts(options = {}) {
847
1059
  } catch (error) {
848
1060
  if (schemaOptionsExplicit) throw error;
849
1061
  logger.warn(`Integration schemas were found, but Farm could not choose a schema target automatically: ${error.message}`);
850
- return;
1062
+ return typeArtifacts;
851
1063
  }
852
1064
  if (!orm) {
853
1065
  if (!schemaOptionsExplicit) {
854
1066
  logger.warn("Integration schemas were found, but no data layer was detected. Pass --orm prisma|drizzle|postgres|mysql|sqlite|mongodb to generate schema artifacts.");
855
- return;
1067
+ return typeArtifacts;
856
1068
  }
857
1069
  throw new Error("Could not auto-detect a schema target. Pass one explicitly with --orm prisma|drizzle|postgres|mysql|sqlite|mongodb.");
858
1070
  }
@@ -863,7 +1075,7 @@ async function generateFarmArtifacts(options = {}) {
863
1075
  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.`);
864
1076
  await writePrismaSchema(schemaPath, collectedModels);
865
1077
  logger.success(`Generated Prisma integration schema in ${path.relative(root, schemaPath)}.`);
866
- return;
1078
+ return typeArtifacts;
867
1079
  }
868
1080
  case "drizzle": {
869
1081
  const dialect = options.dialect ?? await detectDrizzleDialect(root, packageManifest) ?? void 0;
@@ -871,7 +1083,7 @@ async function generateFarmArtifacts(options = {}) {
871
1083
  const outputPath = options.output ? path.resolve(root, options.output) : path.join(root, "farm-integrations.generated.ts");
872
1084
  await writeGeneratedFile(outputPath, generateDrizzleSchema(collectedModels, dialect));
873
1085
  logger.success(`Generated Drizzle integration schema in ${path.relative(root, outputPath)}.`);
874
- return;
1086
+ return typeArtifacts;
875
1087
  }
876
1088
  case "postgres":
877
1089
  case "mysql":
@@ -879,13 +1091,13 @@ async function generateFarmArtifacts(options = {}) {
879
1091
  const outputPath = options.output ? path.resolve(root, options.output) : path.join(root, `farm-integrations.generated.${orm}.sql`);
880
1092
  await writeGeneratedFile(outputPath, generateSqlSchema(collectedModels, orm));
881
1093
  logger.success(`Generated ${orm} integration schema in ${path.relative(root, outputPath)}.`);
882
- return;
1094
+ return typeArtifacts;
883
1095
  }
884
1096
  case "mongodb": {
885
1097
  const outputPath = options.output ? path.resolve(root, options.output) : path.join(root, "farm-integrations.generated.mongodb.ts");
886
1098
  await writeGeneratedFile(outputPath, generateMongoBootstrap(collectedModels));
887
1099
  logger.success(`Generated MongoDB integration bootstrap in ${path.relative(root, outputPath)}.`);
888
- return;
1100
+ return typeArtifacts;
889
1101
  }
890
1102
  }
891
1103
  }
@@ -1070,7 +1282,7 @@ function cloneSchemaModel(model) {
1070
1282
  async function writePrismaSchema(schemaPath, models) {
1071
1283
  const source = await readFile(schemaPath, "utf8");
1072
1284
  const generated = createPrismaGeneratedBlock(generatePrismaSchema(models));
1073
- const pattern = new RegExp(`${escapeRegExp(PRISMA_GENERATED_START)}[\\s\\S]*?${escapeRegExp(PRISMA_GENERATED_END)}`, "m");
1285
+ const pattern = new RegExp(`${escapeRegExp$1(PRISMA_GENERATED_START)}[\\s\\S]*?${escapeRegExp$1(PRISMA_GENERATED_END)}`, "m");
1074
1286
  const nextSource = pattern.test(source) ? source.replace(pattern, generated) : `${source.trimEnd()}\n\n${generated}\n`;
1075
1287
  await writeFile(schemaPath, nextSource, "utf8");
1076
1288
  }
@@ -1398,16 +1610,17 @@ function toCamelCase(value) {
1398
1610
  function escapeString(value) {
1399
1611
  return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/'/g, "''");
1400
1612
  }
1401
- function escapeRegExp(value) {
1613
+ function escapeRegExp$1(value) {
1402
1614
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1403
1615
  }
1404
1616
  //#endregion
1405
1617
  //#region src/doctor.ts
1406
- const ROUTE_EXTENSIONS = [
1618
+ const ROUTE_EXTENSIONS$1 = [
1407
1619
  "ts",
1408
1620
  "tsx",
1409
1621
  "js",
1410
1622
  "jsx",
1623
+ "vue",
1411
1624
  "md",
1412
1625
  "mdx"
1413
1626
  ];
@@ -1425,7 +1638,7 @@ async function runFarmDoctor(options = {}) {
1425
1638
  const root = path.resolve(options.root || process.cwd());
1426
1639
  const liveTarget = resolveLiveTarget(options);
1427
1640
  let liveError;
1428
- if (!options.offline) try {
1641
+ if (!options.offline && !options.fix) try {
1429
1642
  return createLiveReport(await fetchLiveSnapshot(liveTarget, options), liveTarget, options.now);
1430
1643
  } catch (error) {
1431
1644
  liveError = formatError$2(error);
@@ -1472,6 +1685,13 @@ function formatFarmDoctorReport(report, options = {}) {
1472
1685
  `${report.summary.fail} failed`,
1473
1686
  `${report.summary.info} info`
1474
1687
  ].join(" / ");
1688
+ if (report.fixes?.length) {
1689
+ lines.push("", color.bold("FIXED"));
1690
+ for (const fix of report.fixes) {
1691
+ lines.push(` ${color.green("✓")} ${fix.title}`);
1692
+ lines.push(` ${color.dim(fix.filePath)}`);
1693
+ }
1694
+ }
1475
1695
  lines.push("", `${color.bold("SUMMARY")} ${summary}`);
1476
1696
  if (report.target?.devtoolsUrl) lines.push(`${color.bold("DEVTOOLS")} ${report.target.devtoolsUrl}`);
1477
1697
  return lines.join("\n");
@@ -1571,9 +1791,10 @@ async function createProjectReport(root, options) {
1571
1791
  action: "Add farm.config.ts and export defineConfig({...})."
1572
1792
  });
1573
1793
  else {
1794
+ const configRoot = path.resolve(root, userConfig.root || ".");
1574
1795
  config = await resolveConfig({
1575
- root,
1576
- ...userConfig
1796
+ ...userConfig,
1797
+ root: configRoot
1577
1798
  }, "development");
1578
1799
  const configFile = findConfigFile(root, options.configPath);
1579
1800
  checks.push({
@@ -1597,9 +1818,41 @@ async function createProjectReport(root, options) {
1597
1818
  report.target = collectDeploymentChecks(config, userConfig, checks);
1598
1819
  collectCronChecks(config, options.env || process.env, checks);
1599
1820
  }
1821
+ if (config && options.fix) {
1822
+ const fixes = applySafeProjectFixes(root, config, checks);
1823
+ if (fixes.length) {
1824
+ const refreshed = await createProjectReport(root, {
1825
+ ...options,
1826
+ fix: false
1827
+ });
1828
+ refreshed.fixes = fixes;
1829
+ return refreshed;
1830
+ }
1831
+ report.fixes = [];
1832
+ }
1600
1833
  finalizeReport(report);
1601
1834
  return report;
1602
1835
  }
1836
+ function applySafeProjectFixes(root, config, checks) {
1837
+ const fixes = [];
1838
+ if (checks.some((check) => check.code === "ROOT_LAYOUT_MISSING")) {
1839
+ const rendererExtension = config.renderer.componentExtensions?.[0] || ".tsx";
1840
+ const layoutPath = path.join(config.root, config.srcDir, "app", `layout${rendererExtension}`);
1841
+ if (!existsSync(layoutPath)) {
1842
+ mkdirSync(path.dirname(layoutPath), { recursive: true });
1843
+ 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`, {
1844
+ encoding: "utf8",
1845
+ flag: "wx"
1846
+ });
1847
+ fixes.push({
1848
+ code: "ROOT_LAYOUT_CREATED",
1849
+ title: "Created the missing root layout",
1850
+ filePath: path.relative(root, layoutPath)
1851
+ });
1852
+ }
1853
+ }
1854
+ return fixes;
1855
+ }
1603
1856
  function collectNodeCheck(checks) {
1604
1857
  const major = Number(process.versions.node.split(".")[0]);
1605
1858
  checks.push(major >= 18 ? {
@@ -1659,8 +1912,8 @@ function collectPackageCheck(root, checks) {
1659
1912
  function collectRouterChecks(config, checks) {
1660
1913
  const sources = getFarmSourceRoots(config);
1661
1914
  const appDirectories = sources.map((source) => path.join(source.root, source.srcDir, "app"));
1662
- const hasPages = appDirectories.some((directory) => containsFile(directory, /^page\.(?:ts|tsx|js|jsx|md|mdx)$/));
1663
- const hasProgrammaticRoutes = sources.some((source) => ROUTE_EXTENSIONS.some((extension) => existsSync(path.join(source.root, source.srcDir, `farm.routes.${extension}`))));
1915
+ const hasPages = appDirectories.some((directory) => containsFile(directory, /^page\.(?:ts|tsx|js|jsx|vue|md|mdx)$/));
1916
+ const hasProgrammaticRoutes = sources.some((source) => ROUTE_EXTENSIONS$1.some((extension) => existsSync(path.join(source.root, source.srcDir, `farm.routes.${extension}`))));
1664
1917
  checks.push(hasPages || hasProgrammaticRoutes ? {
1665
1918
  status: "pass",
1666
1919
  code: "APP_ROUTER_READY",
@@ -1673,7 +1926,8 @@ function collectRouterChecks(config, checks) {
1673
1926
  message: `Farm found no page modules under ${config.srcDir}/app.`,
1674
1927
  action: `Add ${config.srcDir}/app/page.tsx or ${config.srcDir}/farm.routes.tsx.`
1675
1928
  });
1676
- const hasRootLayout = appDirectories.some((directory) => ROUTE_EXTENSIONS.some((extension) => existsSync(path.join(directory, `layout.${extension}`))));
1929
+ const hasRootLayout = appDirectories.some((directory) => ROUTE_EXTENSIONS$1.some((extension) => existsSync(path.join(directory, `layout.${extension}`))));
1930
+ const suggestedLayoutExtension = config.renderer.componentExtensions?.[0] || ".tsx";
1677
1931
  checks.push(hasRootLayout ? {
1678
1932
  status: "pass",
1679
1933
  code: "ROOT_LAYOUT_READY",
@@ -1684,7 +1938,7 @@ function collectRouterChecks(config, checks) {
1684
1938
  code: "ROOT_LAYOUT_MISSING",
1685
1939
  title: "Root layout is missing",
1686
1940
  message: "The application has no shared root layout.",
1687
- action: `Add ${config.srcDir}/app/layout.tsx.`
1941
+ action: `Add ${config.srcDir}/app/layout${suggestedLayoutExtension}.`
1688
1942
  });
1689
1943
  }
1690
1944
  function collectDeploymentChecks(config, userConfig, checks) {
@@ -1743,7 +1997,7 @@ function hasCronRoute(config, job) {
1743
1997
  const relative = job.path.replace(/^\/+/, "").replace(/^api\//, "");
1744
1998
  return getFarmSourceRoots(config).some((source) => {
1745
1999
  const directory = path.join(source.root, source.srcDir, "app", "api", relative);
1746
- return ROUTE_EXTENSIONS.some((extension) => existsSync(path.join(directory, `route.${extension}`)));
2000
+ return ROUTE_EXTENSIONS$1.some((extension) => existsSync(path.join(directory, `route.${extension}`)));
1747
2001
  });
1748
2002
  }
1749
2003
  function containsFile(directory, pattern) {
@@ -1811,6 +2065,415 @@ function formatCount$1(value, noun) {
1811
2065
  return `${value} ${noun}${value === 1 ? "" : "s"}`;
1812
2066
  }
1813
2067
  //#endregion
2068
+ //#region src/explain.ts
2069
+ const ROUTE_EXTENSIONS = [
2070
+ "tsx",
2071
+ "ts",
2072
+ "jsx",
2073
+ "js",
2074
+ "vue",
2075
+ "mdx",
2076
+ "md"
2077
+ ];
2078
+ const MIDDLEWARE_EXTENSIONS = [
2079
+ "ts",
2080
+ "tsx",
2081
+ "js",
2082
+ "jsx",
2083
+ "mjs",
2084
+ "cjs"
2085
+ ];
2086
+ const SOCIAL_IMAGE_EXTENSIONS = [
2087
+ "tsx",
2088
+ "ts",
2089
+ "jsx",
2090
+ "js",
2091
+ "png",
2092
+ "jpg",
2093
+ "jpeg",
2094
+ "gif",
2095
+ "webp"
2096
+ ];
2097
+ async function explainFarmRoute(pathname, options = {}) {
2098
+ const root = path.resolve(options.root || process.cwd());
2099
+ const userConfig = await loadConfig(root, options.configPath, "production");
2100
+ const config = await resolveConfig({
2101
+ root,
2102
+ ...userConfig
2103
+ }, "production");
2104
+ const normalizedPathname = normalizePathname(pathname, config.basePath || "/");
2105
+ const page = discoverMatchingPages(config, normalizedPathname).sort((left, right) => right.score - left.score || right.priority - left.priority)[0];
2106
+ if (!page) throw new Error(`No Farm page route matches ${normalizedPathname}.`);
2107
+ const layouts = collectInheritedRouteFiles(config, normalizedPathname, "layout", ROUTE_EXTENSIONS);
2108
+ const middleware = collectMiddleware(root, Boolean(userConfig?.middleware && Object.keys(userConfig.middleware).length), config, normalizedPathname);
2109
+ const pageSource = readFileSync(page.filePath, "utf8");
2110
+ const layoutSources = layouts.map((filePath) => ({
2111
+ filePath,
2112
+ source: readFileSync(filePath, "utf8")
2113
+ }));
2114
+ const runtime = resolveFarmRouteRuntimeConfig(mergeFarmRouteRuntimeConfigs(resolveFarmRouteRuleRuntimeConfig(normalizedPathname, config.routeRules), ...layoutSources.map(({ source }) => readRuntimeExports(source)), readRuntimeExports(pageSource)), `Route ${page.pattern}`);
2115
+ const matchingRules = Object.entries(config.routeRules).filter(([pattern]) => farmRouteRuleMatches(pattern, normalizedPathname)).sort(([left], [right]) => routeSpecificity(left) - routeSpecificity(right));
2116
+ const rendering = resolveRendering(pageSource, matchingRules);
2117
+ const cache = resolveCaching(pageSource, matchingRules);
2118
+ const metadataSources = [...layoutSources, {
2119
+ filePath: page.filePath,
2120
+ source: pageSource
2121
+ }];
2122
+ const openGraphImage = findNearestSocialImage(config, normalizedPathname, "opengraph-image");
2123
+ const twitterImage = findNearestSocialImage(config, normalizedPathname, "twitter-image");
2124
+ const preset = String(config.deploy.preset || config.preset || "node-server");
2125
+ const presetRuntime = getFarmPresetRuntime(preset);
2126
+ const compatible = rendering.mode === "static" || rendering.mode === "client" || runtime.runtime === "auto" || presetRuntime !== "unknown" && runtime.runtime === presetRuntime;
2127
+ const warnings = [];
2128
+ if (presetRuntime === "unknown" && runtime.runtime !== "auto") warnings.push(`Farm cannot verify the ${runtime.runtime} route requirement because the ${preset} preset runtime is unknown.`);
2129
+ else if (!compatible) warnings.push(`The route requires ${runtime.runtime}, but the ${preset} preset emits ${presetRuntime} functions.`);
2130
+ if (runtime.regions?.length && preset !== "vercel" && preset !== "vercel-edge") warnings.push(`${preset} does not map Farm per-route region hints.`);
2131
+ if (runtime.maxDuration && preset !== "vercel") warnings.push(`${preset} does not map Farm per-route maxDuration.`);
2132
+ return {
2133
+ pathname: normalizedPathname,
2134
+ pattern: page.pattern,
2135
+ params: page.params,
2136
+ filePath: toProjectPath(root, page.filePath),
2137
+ source: page.source,
2138
+ layouts: layouts.map((filePath) => toProjectPath(root, filePath)),
2139
+ middleware,
2140
+ runtime,
2141
+ rendering,
2142
+ cache,
2143
+ metadata: {
2144
+ static: metadataSources.filter(({ source }) => /export\s+const\s+metadata\b/.test(source)).map(({ filePath }) => toProjectPath(root, filePath)),
2145
+ 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)),
2146
+ ...openGraphImage ? { openGraphImage: toProjectPath(root, openGraphImage) } : {},
2147
+ ...twitterImage ? { twitterImage: toProjectPath(root, twitterImage) } : {}
2148
+ },
2149
+ deployment: {
2150
+ target: String(config.deploy.target || "node"),
2151
+ preset,
2152
+ runtime: presetRuntime,
2153
+ compatible,
2154
+ warnings
2155
+ }
2156
+ };
2157
+ }
2158
+ function formatFarmRouteExplanation(explanation, options = {}) {
2159
+ const color = options.color === void 0 ? pc : pc.createColors(options.color);
2160
+ const lines = [
2161
+ color.bold("FARM / EXPLAIN"),
2162
+ "",
2163
+ `${color.bold("Path")} ${explanation.pathname}`,
2164
+ `${color.bold("Pattern")} ${explanation.pattern}`,
2165
+ `${color.bold("File")} ${explanation.filePath}`,
2166
+ `${color.bold("Source")} ${explanation.source}`,
2167
+ `${color.bold("Params")} ${formatParams(explanation.params)}`,
2168
+ `${color.bold("Layouts")} ${explanation.layouts.length ? explanation.layouts.join(" -> ") : "none"}`,
2169
+ `${color.bold("Middleware")} ${explanation.middleware.length ? explanation.middleware.map((entry) => entry.filePath).join(", ") : "none"}`,
2170
+ `${color.bold("Runtime")} ${formatRuntime(explanation.runtime)}`,
2171
+ `${color.bold("Rendering")} ${explanation.rendering.mode} (${explanation.rendering.reason})${explanation.rendering.ppr ? ", PPR" : ""}`,
2172
+ `${color.bold("Caching")} ${formatCaching(explanation.cache)}`,
2173
+ `${color.bold("Metadata")} ${formatMetadata(explanation.metadata)}`,
2174
+ `${color.bold("Deployment")} ${explanation.deployment.target} / ${explanation.deployment.preset} — ${explanation.deployment.compatible ? color.green("compatible") : color.red("incompatible")}`
2175
+ ];
2176
+ for (const warning of explanation.deployment.warnings) lines.push(` ${color.yellow("!")} ${warning}`);
2177
+ return lines.join("\n");
2178
+ }
2179
+ function discoverMatchingPages(config, pathname) {
2180
+ const candidates = [];
2181
+ for (const [priority, source] of getFarmSourceRoots(config).entries()) {
2182
+ const appDirectory = path.join(source.root, source.srcDir, "app");
2183
+ if (existsSync(appDirectory)) for (const filePath of walkFiles(appDirectory)) {
2184
+ if (!/^page\.(?:tsx?|jsx?|vue|svelte|mdx?)$/.test(path.basename(filePath))) continue;
2185
+ const relativeDirectory = path.relative(appDirectory, path.dirname(filePath));
2186
+ if (relativeDirectory.split(path.sep).includes("api") || isRouteSlotDirectory(relativeDirectory)) continue;
2187
+ const pattern = directoryToRoutePattern(relativeDirectory);
2188
+ const match = matchRoutePattern(pattern, pathname);
2189
+ if (!match) continue;
2190
+ candidates.push({
2191
+ filePath,
2192
+ pattern,
2193
+ params: match.params,
2194
+ score: match.score,
2195
+ source: source.name,
2196
+ priority
2197
+ });
2198
+ }
2199
+ const sourceDirectory = path.join(source.root, source.srcDir);
2200
+ if (!existsSync(sourceDirectory)) continue;
2201
+ for (const filePath of walkFiles(sourceDirectory)) {
2202
+ if (!/\.(?:tsx?|jsx?)$/.test(filePath) || filePath.endsWith(".d.ts")) continue;
2203
+ const moduleSource = readFileSync(filePath, "utf8");
2204
+ for (const pattern of scanProgrammaticPagePaths(moduleSource)) {
2205
+ const match = matchRoutePattern(pattern, pathname);
2206
+ if (!match) continue;
2207
+ candidates.push({
2208
+ filePath,
2209
+ pattern,
2210
+ params: match.params,
2211
+ score: match.score,
2212
+ source: source.name,
2213
+ priority
2214
+ });
2215
+ }
2216
+ }
2217
+ }
2218
+ return candidates;
2219
+ }
2220
+ function isRouteSlotDirectory(relativeDirectory) {
2221
+ return relativeDirectory.split(path.sep).some((segment) => /^@[A-Za-z][\w-]*$/.test(segment));
2222
+ }
2223
+ function walkFiles(directory) {
2224
+ const files = [];
2225
+ const pending = [directory];
2226
+ while (pending.length) {
2227
+ const current = pending.pop();
2228
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
2229
+ const entryPath = path.join(current, entry.name);
2230
+ if (entry.isDirectory()) {
2231
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
2232
+ pending.push(entryPath);
2233
+ continue;
2234
+ }
2235
+ files.push(entryPath);
2236
+ }
2237
+ }
2238
+ return files;
2239
+ }
2240
+ function directoryToRoutePattern(relativeDirectory) {
2241
+ const segments = relativeDirectory.split(path.sep).filter(Boolean).filter((segment) => !/^\(.+\)$/.test(segment) && !segment.startsWith("@")).map((segment) => segment.replace(/^\(\.{1,3}\)/, ""));
2242
+ return segments.length ? `/${segments.join("/")}` : "/";
2243
+ }
2244
+ function matchRoutePattern(pattern, pathname) {
2245
+ const patternSegments = splitPath(pattern);
2246
+ const pathSegments = splitPath(pathname);
2247
+ const params = {};
2248
+ let score = 0;
2249
+ let pathIndex = 0;
2250
+ for (const segment of patternSegments) {
2251
+ const optionalCatchAll = segment.match(/^\[\[\.\.\.(.+)\]\]$/);
2252
+ if (optionalCatchAll) {
2253
+ params[optionalCatchAll[1]] = pathSegments.slice(pathIndex);
2254
+ pathIndex = pathSegments.length;
2255
+ score += 1;
2256
+ continue;
2257
+ }
2258
+ const catchAll = segment.match(/^\[\.\.\.(.+)\]$/);
2259
+ if (catchAll) {
2260
+ if (pathIndex >= pathSegments.length) return null;
2261
+ params[catchAll[1]] = pathSegments.slice(pathIndex);
2262
+ pathIndex = pathSegments.length;
2263
+ score += 10;
2264
+ continue;
2265
+ }
2266
+ const dynamic = segment.match(/^\[(.+)\]$/);
2267
+ if (dynamic) {
2268
+ if (pathIndex >= pathSegments.length) return null;
2269
+ params[dynamic[1]] = pathSegments[pathIndex++];
2270
+ score += 50;
2271
+ continue;
2272
+ }
2273
+ if (segment !== pathSegments[pathIndex++]) return null;
2274
+ score += 100;
2275
+ }
2276
+ return pathIndex === pathSegments.length ? {
2277
+ params,
2278
+ score
2279
+ } : null;
2280
+ }
2281
+ function collectInheritedRouteFiles(config, pathname, baseName, extensions) {
2282
+ return collectLayeredRouteFiles(config, baseName, extensions).filter((entry) => matchesRoutePrefix(entry.pattern, pathname)).sort(compareInheritedRouteFiles).map((entry) => entry.filePath);
2283
+ }
2284
+ function collectMiddleware(root, hasConfigMiddleware, config, pathname) {
2285
+ const rootMiddleware = /* @__PURE__ */ new Map();
2286
+ for (const source of getFarmSourceRoots(config)) {
2287
+ const filePath = findFile(path.join(source.root, source.srcDir), "middleware", MIDDLEWARE_EXTENSIONS);
2288
+ if (filePath) rootMiddleware.set("root", {
2289
+ filePath,
2290
+ pattern: "/"
2291
+ });
2292
+ }
2293
+ const files = [...rootMiddleware.values(), ...collectLayeredRouteFiles(config, "middleware", MIDDLEWARE_EXTENSIONS).filter((entry) => matchesRoutePrefix(entry.pattern, pathname))].sort(compareInheritedRouteFiles).map((entry) => entry.filePath);
2294
+ return [...hasConfigMiddleware ? [{
2295
+ source: "config",
2296
+ filePath: "farm.config (middleware)"
2297
+ }] : [], ...files.map((filePath) => ({
2298
+ source: "file",
2299
+ filePath: toProjectPath(root, filePath)
2300
+ }))];
2301
+ }
2302
+ function collectLayeredRouteFiles(config, baseName, extensions) {
2303
+ const files = /* @__PURE__ */ new Map();
2304
+ const filePattern = new RegExp(`^${escapeRegExp(baseName)}\\.(?:${extensions.map(escapeRegExp).join("|")})$`);
2305
+ for (const source of getFarmSourceRoots(config)) {
2306
+ const appDirectory = path.join(source.root, source.srcDir, "app");
2307
+ if (!existsSync(appDirectory)) continue;
2308
+ for (const filePath of walkFiles(appDirectory)) {
2309
+ if (!filePattern.test(path.basename(filePath))) continue;
2310
+ const pattern = directoryToRoutePattern(path.relative(appDirectory, path.dirname(filePath)));
2311
+ files.set(pattern, {
2312
+ filePath,
2313
+ pattern
2314
+ });
2315
+ }
2316
+ }
2317
+ return [...files.values()];
2318
+ }
2319
+ function findNearestSocialImage(config, pathname, baseName) {
2320
+ return collectLayeredRouteFiles(config, baseName, SOCIAL_IMAGE_EXTENSIONS).filter((entry) => matchesRoutePrefix(entry.pattern, pathname)).sort((left, right) => compareInheritedRouteFiles(right, left))[0]?.filePath;
2321
+ }
2322
+ function compareInheritedRouteFiles(left, right) {
2323
+ return splitPath(left.pattern).length - splitPath(right.pattern).length;
2324
+ }
2325
+ function matchesRoutePrefix(pattern, pathname) {
2326
+ const patternSegments = splitPath(pattern);
2327
+ const pathSegments = splitPath(pathname);
2328
+ if (patternSegments.length > pathSegments.length) return false;
2329
+ return patternSegments.every((segment, index) => {
2330
+ if (/^\[{1,2}(?:\.\.\.)?.+\]{1,2}$/.test(segment)) return true;
2331
+ return segment === pathSegments[index];
2332
+ });
2333
+ }
2334
+ function findFile(directory, baseName, extensions) {
2335
+ return extensions.map((extension) => path.join(directory, `${baseName}.${extension}`)).find(existsSync);
2336
+ }
2337
+ function escapeRegExp(value) {
2338
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2339
+ }
2340
+ function readRuntimeExports(source) {
2341
+ const runtime = readStringExport(source, "runtime");
2342
+ const maxDuration = readNumberOrAutoExport(source, "maxDuration");
2343
+ const regions = readStringArrayOrAutoExport(source, "regions");
2344
+ return {
2345
+ ...runtime === "auto" || runtime === "node" || runtime === "edge" ? { runtime } : {},
2346
+ ...regions ? { regions } : {},
2347
+ ...maxDuration !== void 0 ? { maxDuration } : {}
2348
+ };
2349
+ }
2350
+ function resolveRendering(pageSource, matchingRules) {
2351
+ const pageRendering = resolveRouteRenderingConfig({
2352
+ ...readBooleanExport(pageSource, "ssg") !== void 0 ? { ssg: readBooleanExport(pageSource, "ssg") } : {},
2353
+ ...readBooleanExport(pageSource, "ppr") !== void 0 ? { ppr: readBooleanExport(pageSource, "ppr") } : {},
2354
+ ...readBooleanExport(pageSource, "experimental_ppr") !== void 0 ? { experimental_ppr: readBooleanExport(pageSource, "experimental_ppr") } : {},
2355
+ ...readNumberOrFalseExport(pageSource, "revalidate") !== void 0 ? { revalidate: readNumberOrFalseExport(pageSource, "revalidate") } : {},
2356
+ ...readDynamicExport(pageSource) ? { dynamic: readDynamicExport(pageSource) } : {}
2357
+ }, pageSource);
2358
+ let mode = pageRendering.ssg ? "static" : pageRendering.ppr ? "partial" : "dynamic";
2359
+ let reason = pageRendering.directive ? `page directive ${JSON.stringify(pageRendering.directive)}` : pageRendering.ssg ? "page static rendering declaration" : pageRendering.ppr ? "page PPR declaration" : "default server rendering";
2360
+ let ppr = pageRendering.ppr;
2361
+ for (const [pattern, rule] of matchingRules) if (rule.prerender === true || rule.render === "static") {
2362
+ mode = "static";
2363
+ reason = `routeRules ${pattern}`;
2364
+ ppr = false;
2365
+ } else if (rule.prerender === false || rule.render === "dynamic" || rule.ssr === true) {
2366
+ mode = "dynamic";
2367
+ reason = `routeRules ${pattern}`;
2368
+ ppr = false;
2369
+ } else if (rule.ssr === false) {
2370
+ mode = "client";
2371
+ reason = `routeRules ${pattern}`;
2372
+ ppr = false;
2373
+ }
2374
+ return {
2375
+ mode,
2376
+ reason,
2377
+ ppr
2378
+ };
2379
+ }
2380
+ function resolveCaching(pageSource, matchingRules) {
2381
+ let swr;
2382
+ let isr;
2383
+ for (const [, rule] of matchingRules) {
2384
+ if (typeof rule.swr === "number" || typeof rule.swr === "boolean") swr = rule.swr;
2385
+ if (typeof rule.isr === "number" || typeof rule.isr === "boolean") isr = rule.isr;
2386
+ }
2387
+ return {
2388
+ ...readNumberOrFalseExport(pageSource, "revalidate") !== void 0 ? { revalidate: readNumberOrFalseExport(pageSource, "revalidate") } : {},
2389
+ ...swr !== void 0 ? { swr } : {},
2390
+ ...isr !== void 0 ? { isr } : {},
2391
+ rules: matchingRules.map(([pattern]) => pattern)
2392
+ };
2393
+ }
2394
+ function readStringExport(source, name) {
2395
+ return source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*["']([^"']+)["']`))?.[1];
2396
+ }
2397
+ function readDynamicExport(source) {
2398
+ const dynamic = readStringExport(source, "dynamic");
2399
+ return dynamic === "auto" || dynamic === "force-dynamic" || dynamic === "error" || dynamic === "force-static" ? dynamic : void 0;
2400
+ }
2401
+ function readBooleanExport(source, name) {
2402
+ const value = source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*(true|false)`))?.[1];
2403
+ return value === void 0 ? void 0 : value === "true";
2404
+ }
2405
+ function readNumberOrAutoExport(source, name) {
2406
+ const value = source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*(?:["'](auto)["']|(\\d+))`));
2407
+ return value?.[1] === "auto" ? "auto" : value?.[2] ? Number(value[2]) : void 0;
2408
+ }
2409
+ function readNumberOrFalseExport(source, name) {
2410
+ const value = source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*(false|\\d+)`))?.[1];
2411
+ return value === "false" ? false : value ? Number(value) : void 0;
2412
+ }
2413
+ function readStringArrayOrAutoExport(source, name) {
2414
+ if (source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*["']auto["']`))) return "auto";
2415
+ const array = source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*\\[([^\\]]*)\\]`))?.[1];
2416
+ if (array === void 0) return void 0;
2417
+ return [...array.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
2418
+ }
2419
+ function routeSpecificity(pattern) {
2420
+ return splitPath(pattern).reduce((score, segment) => {
2421
+ if (segment === "**" || segment.startsWith("[[...")) return score + 1;
2422
+ if (segment === "*" || segment.startsWith("[...")) return score + 10;
2423
+ if (segment.startsWith("[") || segment.startsWith(":")) return score + 50;
2424
+ return score + 100;
2425
+ }, 0);
2426
+ }
2427
+ function normalizePathname(value, basePath) {
2428
+ let pathname;
2429
+ try {
2430
+ pathname = new URL(value, "http://farm.local").pathname;
2431
+ } catch {
2432
+ pathname = value;
2433
+ }
2434
+ pathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
2435
+ const normalizedBase = basePath && basePath !== "/" ? `/${basePath.replace(/^\/+|\/+$/g, "")}` : "";
2436
+ if (normalizedBase && (pathname === normalizedBase || pathname.startsWith(`${normalizedBase}/`))) pathname = pathname.slice(normalizedBase.length) || "/";
2437
+ return pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
2438
+ }
2439
+ function splitPath(value) {
2440
+ return value.split("/").filter(Boolean).map(decodeURIComponent);
2441
+ }
2442
+ function toProjectPath(root, filePath) {
2443
+ let relativePath = path.relative(root, filePath);
2444
+ if ((relativePath === ".." || relativePath.startsWith(`..${path.sep}`)) && existsSync(root) && existsSync(filePath)) relativePath = path.relative(realpathSync(root), realpathSync(filePath));
2445
+ return relativePath.split(path.sep).join("/");
2446
+ }
2447
+ function formatParams(params) {
2448
+ const entries = Object.entries(params);
2449
+ return entries.length ? entries.map(([key, value]) => `${key}=${Array.isArray(value) ? value.join("/") : value}`).join(", ") : "none";
2450
+ }
2451
+ function formatRuntime(runtime) {
2452
+ return [
2453
+ runtime.runtime,
2454
+ runtime.regions?.length ? `regions=${runtime.regions.join(",")}` : "",
2455
+ runtime.maxDuration ? `maxDuration=${runtime.maxDuration}s` : ""
2456
+ ].filter(Boolean).join(", ");
2457
+ }
2458
+ function formatCaching(cache) {
2459
+ const values = [
2460
+ cache.revalidate !== void 0 ? `revalidate=${cache.revalidate}` : "",
2461
+ cache.swr !== void 0 ? `swr=${cache.swr}` : "",
2462
+ cache.isr !== void 0 ? `isr=${cache.isr}` : "",
2463
+ cache.rules.length ? `rules=${cache.rules.join(",")}` : ""
2464
+ ].filter(Boolean);
2465
+ return values.length ? values.join("; ") : "request-time / no declared cache";
2466
+ }
2467
+ function formatMetadata(metadata) {
2468
+ const values = [
2469
+ metadata.static.length ? `static=${metadata.static.join(",")}` : "",
2470
+ metadata.dynamic.length ? `dynamic=${metadata.dynamic.join(",")}` : "",
2471
+ metadata.openGraphImage ? `og=${metadata.openGraphImage}` : "",
2472
+ metadata.twitterImage ? `twitter=${metadata.twitterImage}` : ""
2473
+ ].filter(Boolean);
2474
+ return values.length ? values.join("; ") : "none";
2475
+ }
2476
+ //#endregion
1814
2477
  //#region src/cron.ts
1815
2478
  async function loadFarmCronConfig(options = {}) {
1816
2479
  const root = path.resolve(options.root || process.cwd());
@@ -2420,6 +3083,31 @@ function toPosix(value) {
2420
3083
  return value.split(path.sep).join("/");
2421
3084
  }
2422
3085
  //#endregion
3086
+ //#region src/auth.ts
3087
+ async function migrateFarmAuth(options = {}) {
3088
+ const root = path.resolve(options.root || process.cwd());
3089
+ const userConfig = await loadConfig(root, options.configPath, "production");
3090
+ if (!userConfig) throw new Error(`No farm.config file was found in ${root}.`);
3091
+ if (!(await resolveConfig({
3092
+ ...userConfig,
3093
+ root
3094
+ }, "production")).auth.enabled) throw new Error("Farm Auth is disabled. Add `auth: true` to farm.config.ts first.");
3095
+ const resolveFromApp = createRequire(path.join(root, "package.json"));
3096
+ let modulePath;
3097
+ try {
3098
+ modulePath = resolveFromApp.resolve("@farm.js/auth/internal");
3099
+ } catch {
3100
+ throw new Error("Install @farm.js/auth before running `farm auth migrate`.");
3101
+ }
3102
+ const runtime = await import(
3103
+ /* @vite-ignore */
3104
+ pathToFileURL(modulePath).href
3105
+ );
3106
+ logger.info("Applying the Farm Auth database schema...");
3107
+ await runtime.migrateFarmAuth();
3108
+ logger.success("Farm Auth database is ready.");
3109
+ }
3110
+ //#endregion
2423
3111
  //#region src/upgrade.ts
2424
3112
  const DEPENDENCY_SECTIONS = [
2425
3113
  "dependencies",
@@ -2590,6 +3278,6 @@ function formatError(error) {
2590
3278
  return error instanceof Error ? error.message : String(error);
2591
3279
  }
2592
3280
  //#endregion
2593
- export { addFarmIntegration, buildFarm, createFarmUpgradePlan, createFrameworkMigrationPlan, createGatewaySession, createPreviewGatewayPlan, createPreviewTunnelPlan, createServer, deployFarm, detectFarmPackageManager, formatFarmCronJobs, formatFarmDoctorReport, formatFarmUpgradePlan, forwardGatewayRequest, generateFarmArtifacts, inspectFrameworkMigrations, listFarmCronJobs, listFarmIntegrationProviders, loadFarmCronConfig, migrateFarm, parsePreviewPublicUrl, previewFarm, resolveCloudflareAgentDeployPlan, resolvePreviewTarget, runFarmCronJob, runFarmDoctor, runPreviewGateway, startDevServer, startFarmCronScheduler, upgradeFarm };
3281
+ export { FarmDeployError, FarmGeneratedArtifactsStaleError, addFarmIntegration, buildFarm, createFarmDeployPlan, createFarmUpgradePlan, createFrameworkMigrationPlan, createGatewaySession, createPreviewGatewayPlan, createPreviewTunnelPlan, createServer, deployFarm, detectFarmPackageManager, explainFarmRoute, formatFarmCronJobs, formatFarmDeployPlan, formatFarmDoctorReport, formatFarmRouteExplanation, formatFarmUpgradePlan, forwardGatewayRequest, generateFarmArtifacts, getFarmTelemetryConfigFile, getFarmTelemetryStatus, inspectFrameworkMigrations, listFarmCronJobs, listFarmIntegrationProviders, loadFarmCronConfig, migrateFarm, migrateFarmAuth, parsePreviewPublicUrl, previewFarm, resolveCloudflareAgentDeployPlan, resolveFarmTelemetryCommand, resolvePreviewTarget, runFarmCronJob, runFarmDoctor, runNativePreviewTunnel, runPreviewGateway, setFarmTelemetryEnabled, showFarmTelemetryNotice, startDevServer, startFarmCronScheduler, trackFarmCommand, trackFarmProjectCreated, upgradeFarm };
2594
3282
 
2595
3283
  //# sourceMappingURL=index.mjs.map