@farm.js/cli 0.1.0-beta.6 → 0.1.0-beta.60

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,173 @@
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 { APITypeGenerator, generateEnvTypes, generateFarmI18nTypes, generateRouteTypes, 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
+ import { pathToFileURL } from "node:url";
16
19
  //#region \0rolldown/runtime.js
17
20
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
18
21
  //#endregion
19
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
+ };
20
32
  /**
21
33
  * Deploy Farm.js application
22
34
  */
23
35
  async function deployFarm(options = {}) {
24
- 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());
25
73
  const mode = "production";
26
74
  const userConfig = await loadConfig(root, void 0, mode);
27
- const config = userConfig ? await resolveConfig(userConfig, mode) : void 0;
75
+ const config = await resolveConfig({
76
+ root,
77
+ ...userConfig
78
+ }, mode);
28
79
  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
- }
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.`);
34
87
  const deployConfig = resolveDeployConfig(userConfig || {}, {
35
88
  target: platform,
36
- preset: cliTarget ? userConfig?.deploy?.preset || userConfig?.preset || getPresetForDeployTarget(platform) : void 0
89
+ preset: cliTarget ? presetMatchesPlatform ? configuredPreset : getPresetForDeployTarget(platform) : void 0
37
90
  });
38
91
  const preset = deployConfig.preset || getPresetForDeployTarget(platform) || "node-server";
39
- logger.info(`🚀 Building with ${preset} preset...`);
40
- await buildFarm({
41
- root,
42
- preset
43
- });
44
92
  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);
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
+ };
48
127
  }
49
- 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
+ ];
50
171
  }
51
172
  /**
52
173
  * Deploy using platform's native CLI (user credentials)
@@ -68,19 +189,20 @@ async function deployPlatform(platform, root, outputDir, deployConfig, prod) {
68
189
  async function deployVercel(root, outputDir, prod) {
69
190
  logger.info("🚀 Deploying to Vercel...");
70
191
  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);
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 });
76
198
  }
77
199
  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);
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 });
84
206
  }
85
207
  try {
86
208
  const { existsSync, statSync, readdirSync } = await import("fs");
@@ -89,14 +211,8 @@ async function deployVercel(root, outputDir, prod) {
89
211
  const configFile = path$1.join(outputDir, "config.json");
90
212
  const serverIndex = path$1.join(functionsDir, "index.mjs");
91
213
  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
- }
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}.`);
100
216
  logger.info(`✅ Functions directory: ${functionsDir}`);
101
217
  if (!existsSync(staticDir)) logger.warn(`⚠️ Static directory not found at ${staticDir}`);
102
218
  else {
@@ -177,14 +293,19 @@ async function deployVercel(root, outputDir, prod) {
177
293
  logger.info(` Static: ${staticDir}`);
178
294
  logger.info(` Config: ${configFile}`);
179
295
  logger.info("📤 Uploading to Vercel...");
180
- execSync(`vercel deploy --prebuilt --yes${prod ? " --prod" : ""}`, {
296
+ execFileSync("vercel", [
297
+ "deploy",
298
+ "--prebuilt",
299
+ "--yes",
300
+ ...prod ? ["--prod"] : []
301
+ ], {
181
302
  stdio: "inherit",
182
303
  cwd: root
183
304
  });
184
305
  logger.success("✅ Deployed to Vercel successfully!");
185
306
  } catch (error) {
186
- logger.error(`❌ Failed to deploy to Vercel: ${error.message}`);
187
- 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 });
188
309
  }
189
310
  }
190
311
  /** Deploy to a composed Worker or fall back to Cloudflare Pages. */
@@ -205,8 +326,7 @@ async function deployCloudflare(root, outputDir, projectName) {
205
326
  });
206
327
  logger.success("✅ Deployed Farm and Cloudflare Agents successfully!");
207
328
  } catch (error) {
208
- logger.error(`❌ Failed to deploy to Cloudflare: ${error.message}`);
209
- process.exit(1);
329
+ throw new FarmDeployError("DEPLOY_FAILED", "cloudflare", `Failed to deploy to Cloudflare: ${getErrorMessage(error)}`, { cause: error });
210
330
  }
211
331
  return;
212
332
  }
@@ -224,8 +344,7 @@ async function deployCloudflare(root, outputDir, projectName) {
224
344
  });
225
345
  logger.success("✅ Deployed to Cloudflare Pages successfully!");
226
346
  } catch (error) {
227
- logger.error(`❌ Failed to deploy to Cloudflare: ${error.message}`);
228
- process.exit(1);
347
+ throw new FarmDeployError("DEPLOY_FAILED", "cloudflare", `Failed to deploy to Cloudflare: ${getErrorMessage(error)}`, { cause: error });
229
348
  }
230
349
  }
231
350
  /** Read the trusted Workers deployment handoff emitted by @farm.js/cf-agent. */
@@ -251,16 +370,34 @@ function resolveCloudflareAgentDeployPlan(root) {
251
370
  ...typeof environment === "string" ? { environment: environment.trim() } : {}
252
371
  };
253
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
+ }
254
393
  function assertWranglerInstalled(root) {
255
394
  try {
256
395
  execFileSync("wrangler", ["--version"], {
257
396
  stdio: "ignore",
258
397
  cwd: root
259
398
  });
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);
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 });
264
401
  }
265
402
  }
266
403
  function isRecord(value) {
@@ -272,20 +409,103 @@ function isRecord(value) {
272
409
  async function deployNetlify(root, outputDir, site) {
273
410
  logger.info("🚀 Deploying to Netlify...");
274
411
  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);
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 });
280
418
  }
281
419
  try {
282
- process.chdir(outputDir);
283
- const siteFlag = site ? ` --site=${site}` : "";
284
- execSync(`netlify deploy --prod --dir=.${siteFlag}`, { stdio: "inherit" });
420
+ execFileSync("netlify", createNetlifyDeployArgs(site), {
421
+ stdio: "inherit",
422
+ cwd: outputDir
423
+ });
285
424
  logger.success("✅ Deployed to Netlify successfully!");
286
425
  } catch (error) {
287
- logger.error(`❌ Failed to deploy to Netlify: ${error.message}`);
288
- 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();
289
509
  }
290
510
  }
291
511
  //#endregion
@@ -310,10 +530,12 @@ const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
310
530
  function createPreviewGatewayPlan(target, options = {}) {
311
531
  const requestedName = sanitizePreviewName$1(options.name || process.env.FARM_PREVIEW_NAME) || randomPreviewName$1();
312
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);
313
534
  const requestedHostname = `${requestedName}.${normalizePreviewDomain$1(process.env.FARM_PREVIEW_DOMAIN || DEFAULT_PREVIEW_DOMAIN)}`;
314
535
  return {
315
536
  provider: "farm-gateway",
316
537
  gatewayUrl,
538
+ relayUrl,
317
539
  target,
318
540
  requestedName,
319
541
  requestedHostname,
@@ -422,6 +644,9 @@ async function createGatewaySession(plan, timeoutMs) {
422
644
  if (!session.id || !session.token || !session.publicUrl) throw new Error("Preview gateway returned an invalid session.");
423
645
  return session;
424
646
  }
647
+ function getSetCookies(headers) {
648
+ return headers.getSetCookie?.call(headers) || [];
649
+ }
425
650
  async function forwardGatewayRequest(target, request) {
426
651
  const headers = new Headers();
427
652
  for (const [key, value] of Object.entries(request.headers || {})) {
@@ -440,8 +665,10 @@ async function forwardGatewayRequest(target, request) {
440
665
  const responseHeaders = {};
441
666
  response.headers.forEach((value, key) => {
442
667
  const normalized = key.toLowerCase();
443
- if (!HOP_BY_HOP_HEADERS.has(normalized)) responseHeaders[key] = value;
668
+ if (!HOP_BY_HOP_HEADERS.has(normalized) && normalized !== "set-cookie") responseHeaders[key] = value;
444
669
  });
670
+ const setCookies = getSetCookies(response.headers);
671
+ if (setCookies.length > 0) responseHeaders["set-cookie"] = setCookies;
445
672
  return {
446
673
  status: response.status,
447
674
  headers: responseHeaders,
@@ -478,6 +705,7 @@ async function closeGatewaySession(plan, session) {
478
705
  function formatGatewayPlan(plan) {
479
706
  return [
480
707
  `Gateway: ${plan.gatewayUrl}`,
708
+ `Relay: ${plan.relayUrl}`,
481
709
  `Local: ${plan.target.localUrl}`,
482
710
  `Public: ${plan.requestedPublicUrl}`
483
711
  ].join("\n");
@@ -489,6 +717,17 @@ function formatRequestPath(path) {
489
717
  function normalizeGatewayUrl(value) {
490
718
  return value.replace(/\/+$/, "");
491
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
+ }
492
731
  function normalizePreviewDomain$1(value) {
493
732
  return value.replace(/^https?:\/\//, "").replace(/^\.*/, "").replace(/\/*$/, "");
494
733
  }
@@ -500,6 +739,39 @@ function randomPreviewName$1() {
500
739
  return `farm-${Math.random().toString(36).slice(2, 8)}`;
501
740
  }
502
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
503
775
  //#region src/preview.ts
504
776
  const DEFAULT_PREVIEW_PORTS = [
505
777
  3e3,
@@ -523,15 +795,26 @@ async function previewFarm(options = {}) {
523
795
  plan
524
796
  };
525
797
  }
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
- };
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
+ }
535
818
  }
536
819
  const plan = createPreviewTunnelPlan(target, options);
537
820
  if (options.dryRun) {
@@ -549,6 +832,9 @@ async function previewFarm(options = {}) {
549
832
  publicUrl: await runPreviewTunnel(plan, options.timeoutMs ?? 3e4)
550
833
  };
551
834
  }
835
+ function formatPreviewError(error) {
836
+ return error instanceof Error && error.message ? ` ${error.message}` : "";
837
+ }
552
838
  function shouldUseManagedGateway(options) {
553
839
  if (options.provider === "farm" || process.env.FARM_PREVIEW_PROVIDER === "farm") return true;
554
840
  if (options.provider === "local" || process.env.FARM_PREVIEW_PROVIDER === "local") return false;
@@ -657,7 +943,7 @@ function parsePreviewPublicUrl(output, preferredHostname) {
657
943
  }) || urls[0];
658
944
  }
659
945
  async function runPreviewTunnel(plan, timeoutMs) {
660
- const child = spawn(plan.command, plan.args, {
946
+ const child = spawn$1(plan.command, plan.args, {
661
947
  env: {
662
948
  ...process.env,
663
949
  FARM_PREVIEW_LOCAL_URL: plan.target.localUrl,
@@ -799,10 +1085,20 @@ function normalizePreviewDomain(value) {
799
1085
  }
800
1086
  //#endregion
801
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
+ };
802
1097
  const PRISMA_GENERATED_START = "// Farm.js integrations generated schema: start";
803
1098
  const PRISMA_GENERATED_END = "// Farm.js integrations generated schema: end";
804
1099
  async function generateFarmArtifacts(options = {}) {
805
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.");
806
1102
  const userConfig = await loadConfig(root, options.configPath, "development");
807
1103
  if (!userConfig && hasSchemaOptions(options)) throw new Error("No Farm config found. Please create farm.config.ts or config.ts.");
808
1104
  const resolvedConfig = await resolveConfig({
@@ -810,34 +1106,28 @@ async function generateFarmArtifacts(options = {}) {
810
1106
  ...userConfig
811
1107
  }, "development");
812
1108
  const extraRoutes = [...resolvedConfig.openapi?.enabled && resolvedConfig.openapi.route ? [resolvedConfig.openapi.route] : [], ...getFarmDocsRouteTypeEntries(resolvedConfig.docs)];
813
- await generateRouteTypes({
1109
+ const typeArtifacts = await generateFarmTypeArtifacts({
814
1110
  root: resolvedConfig.root,
815
1111
  srcDir: resolvedConfig.srcDir,
1112
+ configPath: options.configPath,
1113
+ layers: resolvedConfig.layers,
816
1114
  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
1115
+ suppressLintOnLink: resolvedConfig.suppressLintOnLink,
1116
+ componentExtensions: resolvedConfig.renderer.componentExtensions,
1117
+ i18nConfig: resolvedConfig.i18n,
1118
+ check: options.check
834
1119
  });
835
- logger.success(`Generated route, API, env${resolvedConfig.i18n.enabled ? ", and i18n" : ""} types (${apiRoutes.length} API route${apiRoutes.length === 1 ? "" : "s"}).`);
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
+ }
1125
+ logger.success(`Generated route, API, env${resolvedConfig.i18n.enabled ? ", and i18n" : ""} types (${typeArtifacts.apiRoutes.length} API route${typeArtifacts.apiRoutes.length === 1 ? "" : "s"}).`);
836
1126
  const schemas = getIntegrationSchemas(resolvedConfig.integrations);
837
1127
  const schemaEntries = Object.entries(schemas);
838
1128
  if (!schemaEntries.length) {
839
1129
  if (hasSchemaOptions(options)) logger.warn("No integration schemas were found in the current Farm config.");
840
- return;
1130
+ return typeArtifacts;
841
1131
  }
842
1132
  const packageManifest = await readPackageManifest(root);
843
1133
  const schemaOptionsExplicit = hasSchemaOptions(options);
@@ -847,12 +1137,12 @@ async function generateFarmArtifacts(options = {}) {
847
1137
  } catch (error) {
848
1138
  if (schemaOptionsExplicit) throw error;
849
1139
  logger.warn(`Integration schemas were found, but Farm could not choose a schema target automatically: ${error.message}`);
850
- return;
1140
+ return typeArtifacts;
851
1141
  }
852
1142
  if (!orm) {
853
1143
  if (!schemaOptionsExplicit) {
854
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.");
855
- return;
1145
+ return typeArtifacts;
856
1146
  }
857
1147
  throw new Error("Could not auto-detect a schema target. Pass one explicitly with --orm prisma|drizzle|postgres|mysql|sqlite|mongodb.");
858
1148
  }
@@ -863,7 +1153,7 @@ async function generateFarmArtifacts(options = {}) {
863
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.`);
864
1154
  await writePrismaSchema(schemaPath, collectedModels);
865
1155
  logger.success(`Generated Prisma integration schema in ${path.relative(root, schemaPath)}.`);
866
- return;
1156
+ return typeArtifacts;
867
1157
  }
868
1158
  case "drizzle": {
869
1159
  const dialect = options.dialect ?? await detectDrizzleDialect(root, packageManifest) ?? void 0;
@@ -871,7 +1161,7 @@ async function generateFarmArtifacts(options = {}) {
871
1161
  const outputPath = options.output ? path.resolve(root, options.output) : path.join(root, "farm-integrations.generated.ts");
872
1162
  await writeGeneratedFile(outputPath, generateDrizzleSchema(collectedModels, dialect));
873
1163
  logger.success(`Generated Drizzle integration schema in ${path.relative(root, outputPath)}.`);
874
- return;
1164
+ return typeArtifacts;
875
1165
  }
876
1166
  case "postgres":
877
1167
  case "mysql":
@@ -879,13 +1169,13 @@ async function generateFarmArtifacts(options = {}) {
879
1169
  const outputPath = options.output ? path.resolve(root, options.output) : path.join(root, `farm-integrations.generated.${orm}.sql`);
880
1170
  await writeGeneratedFile(outputPath, generateSqlSchema(collectedModels, orm));
881
1171
  logger.success(`Generated ${orm} integration schema in ${path.relative(root, outputPath)}.`);
882
- return;
1172
+ return typeArtifacts;
883
1173
  }
884
1174
  case "mongodb": {
885
1175
  const outputPath = options.output ? path.resolve(root, options.output) : path.join(root, "farm-integrations.generated.mongodb.ts");
886
1176
  await writeGeneratedFile(outputPath, generateMongoBootstrap(collectedModels));
887
1177
  logger.success(`Generated MongoDB integration bootstrap in ${path.relative(root, outputPath)}.`);
888
- return;
1178
+ return typeArtifacts;
889
1179
  }
890
1180
  }
891
1181
  }
@@ -1070,7 +1360,7 @@ function cloneSchemaModel(model) {
1070
1360
  async function writePrismaSchema(schemaPath, models) {
1071
1361
  const source = await readFile(schemaPath, "utf8");
1072
1362
  const generated = createPrismaGeneratedBlock(generatePrismaSchema(models));
1073
- 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");
1074
1364
  const nextSource = pattern.test(source) ? source.replace(pattern, generated) : `${source.trimEnd()}\n\n${generated}\n`;
1075
1365
  await writeFile(schemaPath, nextSource, "utf8");
1076
1366
  }
@@ -1092,18 +1382,18 @@ function renderPrismaModel(model) {
1092
1382
  const defaultAttribute = getPrismaDefaultAttribute(field);
1093
1383
  if (defaultAttribute) attributes.push(defaultAttribute);
1094
1384
  if (field.meta?.autoUpdate && field.type === "datetime") attributes.push("@updatedAt");
1095
- if (field.name !== fieldKey) attributes.push(`@map("${escapeString(field.name)}")`);
1385
+ if (field.name !== fieldKey) attributes.push(`@map("${escapeDoubleQuoted(field.name)}")`);
1096
1386
  if (attributes.length) parts.push(attributes.join(" "));
1097
1387
  lines.push(` ${parts.join(" ")}`);
1098
- 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`)}")`);
1099
1389
  }
1100
1390
  for (const constraint of model.model.constraints || []) {
1101
1391
  const fields = constraint.fields.join(", ");
1102
1392
  const attribute = constraint.type === "unique" ? "@@unique" : "@@index";
1103
- const suffix = constraint.name ? `, map: "${escapeString(constraint.name)}"` : "";
1393
+ const suffix = constraint.name ? `, map: "${escapeDoubleQuoted(constraint.name)}"` : "";
1104
1394
  modelLevelConstraints.push(`${attribute}([${fields}]${suffix})`);
1105
1395
  }
1106
- lines.push(` @@map("${escapeString(model.modelName)}")`);
1396
+ lines.push(` @@map("${escapeDoubleQuoted(model.modelName)}")`);
1107
1397
  for (const constraint of modelLevelConstraints) lines.push(` ${constraint}`);
1108
1398
  lines.push("}");
1109
1399
  return lines.join("\n");
@@ -1121,7 +1411,7 @@ function getPrismaFieldType(field) {
1121
1411
  function getPrismaDefaultAttribute(field) {
1122
1412
  if (field.default === void 0) return null;
1123
1413
  if (field.type === "datetime" && field.default === "now") return "@default(now())";
1124
- if (typeof field.default === "string") return `@default("${escapeString(field.default)}")`;
1414
+ if (typeof field.default === "string") return `@default("${escapeDoubleQuoted(field.default)}")`;
1125
1415
  if (typeof field.default === "number" || typeof field.default === "boolean") return `@default(${String(field.default)})`;
1126
1416
  return null;
1127
1417
  }
@@ -1159,12 +1449,12 @@ function renderDrizzleModel(model, dialect, tableFactoryName) {
1159
1449
  lines.push(` ${fieldKey}: ${renderDrizzleColumn(field, dialect)},`);
1160
1450
  }
1161
1451
  lines.push("}, (table) => ({");
1162
- 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}),`);
1163
1453
  for (const constraint of model.model.constraints || []) {
1164
1454
  const builder = constraint.type === "unique" ? "uniqueIndex" : "index";
1165
1455
  const accessor = constraint.fields.map((fieldKey) => `table.${fieldKey}`).join(", ");
1166
1456
  const name = constraint.name || `${model.modelName}_${constraint.fields.map((fieldKey) => model.model.fields[fieldKey]?.name || fieldKey).join("_")}_${constraint.type}`;
1167
- lines.push(` ${toCamelCase(name)}: ${builder}("${escapeString(name)}").on(${accessor}),`);
1457
+ lines.push(` ${toCamelCase(name)}: ${builder}("${escapeDoubleQuoted(name)}").on(${accessor}),`);
1168
1458
  }
1169
1459
  lines.push("}));");
1170
1460
  return lines.join("\n");
@@ -1242,7 +1532,7 @@ function renderSqliteDrizzleColumn(field) {
1242
1532
  function getDrizzleDefaultExpression(field, dialect) {
1243
1533
  if (field.default === void 0) return "";
1244
1534
  if (field.type === "datetime" && field.default === "now") return dialect === "sqlite" ? "" : ".defaultNow()";
1245
- if (typeof field.default === "string") return `.default("${escapeString(field.default)}")`;
1535
+ if (typeof field.default === "string") return `.default("${escapeDoubleQuoted(field.default)}")`;
1246
1536
  if (typeof field.default === "number" || typeof field.default === "boolean") return `.default(${String(field.default)})`;
1247
1537
  return "";
1248
1538
  }
@@ -1333,7 +1623,7 @@ function getSqlColumnType(field, dialect) {
1333
1623
  function getSqlDefaultExpression(field, dialect) {
1334
1624
  if (field.default === void 0) return null;
1335
1625
  if (field.type === "datetime" && field.default === "now") return "CURRENT_TIMESTAMP";
1336
- if (typeof field.default === "string") return `'${escapeString(field.default)}'`;
1626
+ if (typeof field.default === "string") return `'${escapeSqlString(field.default)}'`;
1337
1627
  if (typeof field.default === "number") return String(field.default);
1338
1628
  if (typeof field.default === "boolean") {
1339
1629
  if (dialect === "sqlite") return field.default ? "1" : "0";
@@ -1350,20 +1640,20 @@ function generateMongoBootstrap(models) {
1350
1640
  ];
1351
1641
  for (const model of models) {
1352
1642
  lines.push(` // Integration "${model.integrationKey}" model "${model.modelKey}"`);
1353
- lines.push(` const ${model.exportName} = db.collection("${escapeString(model.modelName)}");`);
1643
+ lines.push(` const ${model.exportName} = db.collection("${escapeDoubleQuoted(model.modelName)}");`);
1354
1644
  for (const [fieldKey, field] of Object.entries(model.model.fields)) {
1355
1645
  if (field.unique) {
1356
1646
  const options = ["unique: true"];
1357
1647
  if (isNullableField(field)) options.push("sparse: true");
1358
- options.push(`name: "${escapeString(`${model.modelName}_${field.name}_unique`)}"`);
1648
+ options.push(`name: "${escapeDoubleQuoted(`${model.modelName}_${field.name}_unique`)}"`);
1359
1649
  lines.push(` await ${model.exportName}.createIndex({ ${JSON.stringify(field.name)}: 1 }, { ${options.join(", ")} });`);
1360
- } 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`)}" });`);
1361
1651
  if (field.reference) lines.push(` // ${fieldKey} references ${field.reference.model}.${field.reference.field}${field.reference.onDelete ? ` (onDelete: ${field.reference.onDelete})` : ""}`);
1362
1652
  }
1363
1653
  for (const constraint of model.model.constraints || []) {
1364
1654
  const indexSpec = constraint.fields.map((fieldKey) => `${JSON.stringify(model.model.fields[fieldKey]?.name || fieldKey)}: 1`).join(", ");
1365
1655
  const indexName = constraint.name || `${model.modelName}_${constraint.fields.map((fieldKey) => model.model.fields[fieldKey]?.name || fieldKey).join("_")}_${constraint.type}`;
1366
- 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)}" }`;
1367
1657
  lines.push(` await ${model.exportName}.createIndex({ ${indexSpec} }, ${options});`);
1368
1658
  }
1369
1659
  lines.push("");
@@ -1395,19 +1685,23 @@ function toCamelCase(value) {
1395
1685
  const pascal = toPascalCase(value);
1396
1686
  return pascal ? pascal.charAt(0).toLowerCase() + pascal.slice(1) : pascal;
1397
1687
  }
1398
- function escapeString(value) {
1399
- return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/'/g, "''");
1688
+ function escapeDoubleQuoted(value) {
1689
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1400
1690
  }
1401
- function escapeRegExp(value) {
1691
+ function escapeSqlString(value) {
1692
+ return value.replace(/'/g, "''");
1693
+ }
1694
+ function escapeRegExp$1(value) {
1402
1695
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1403
1696
  }
1404
1697
  //#endregion
1405
1698
  //#region src/doctor.ts
1406
- const ROUTE_EXTENSIONS = [
1699
+ const ROUTE_EXTENSIONS$1 = [
1407
1700
  "ts",
1408
1701
  "tsx",
1409
1702
  "js",
1410
1703
  "jsx",
1704
+ "vue",
1411
1705
  "md",
1412
1706
  "mdx"
1413
1707
  ];
@@ -1425,7 +1719,7 @@ async function runFarmDoctor(options = {}) {
1425
1719
  const root = path.resolve(options.root || process.cwd());
1426
1720
  const liveTarget = resolveLiveTarget(options);
1427
1721
  let liveError;
1428
- if (!options.offline) try {
1722
+ if (!options.offline && !options.fix) try {
1429
1723
  return createLiveReport(await fetchLiveSnapshot(liveTarget, options), liveTarget, options.now);
1430
1724
  } catch (error) {
1431
1725
  liveError = formatError$2(error);
@@ -1472,6 +1766,13 @@ function formatFarmDoctorReport(report, options = {}) {
1472
1766
  `${report.summary.fail} failed`,
1473
1767
  `${report.summary.info} info`
1474
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
+ }
1475
1776
  lines.push("", `${color.bold("SUMMARY")} ${summary}`);
1476
1777
  if (report.target?.devtoolsUrl) lines.push(`${color.bold("DEVTOOLS")} ${report.target.devtoolsUrl}`);
1477
1778
  return lines.join("\n");
@@ -1498,6 +1799,10 @@ async function fetchLiveSnapshot(baseUrl, options) {
1498
1799
  clearTimeout(timeout);
1499
1800
  }
1500
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
+ }
1501
1806
  function createLiveReport(snapshot, baseUrl, now) {
1502
1807
  const checks = [
1503
1808
  {
@@ -1571,16 +1876,17 @@ async function createProjectReport(root, options) {
1571
1876
  action: "Add farm.config.ts and export defineConfig({...})."
1572
1877
  });
1573
1878
  else {
1879
+ const configRoot = path.resolve(root, userConfig.root || ".");
1574
1880
  config = await resolveConfig({
1575
- root,
1576
- ...userConfig
1881
+ ...userConfig,
1882
+ root: configRoot
1577
1883
  }, "development");
1578
1884
  const configFile = findConfigFile(root, options.configPath);
1579
1885
  checks.push({
1580
1886
  status: "pass",
1581
1887
  code: "CONFIG_VALID",
1582
1888
  title: "Farm config loads successfully",
1583
- 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"
1584
1890
  });
1585
1891
  }
1586
1892
  } catch (error) {
@@ -1597,9 +1903,41 @@ async function createProjectReport(root, options) {
1597
1903
  report.target = collectDeploymentChecks(config, userConfig, checks);
1598
1904
  collectCronChecks(config, options.env || process.env, checks);
1599
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
+ }
1600
1918
  finalizeReport(report);
1601
1919
  return report;
1602
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
+ }
1603
1941
  function collectNodeCheck(checks) {
1604
1942
  const major = Number(process.versions.node.split(".")[0]);
1605
1943
  checks.push(major >= 18 ? {
@@ -1659,8 +1997,8 @@ function collectPackageCheck(root, checks) {
1659
1997
  function collectRouterChecks(config, checks) {
1660
1998
  const sources = getFarmSourceRoots(config);
1661
1999
  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}`))));
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}`))));
1664
2002
  checks.push(hasPages || hasProgrammaticRoutes ? {
1665
2003
  status: "pass",
1666
2004
  code: "APP_ROUTER_READY",
@@ -1673,7 +2011,8 @@ function collectRouterChecks(config, checks) {
1673
2011
  message: `Farm found no page modules under ${config.srcDir}/app.`,
1674
2012
  action: `Add ${config.srcDir}/app/page.tsx or ${config.srcDir}/farm.routes.tsx.`
1675
2013
  });
1676
- 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";
1677
2016
  checks.push(hasRootLayout ? {
1678
2017
  status: "pass",
1679
2018
  code: "ROOT_LAYOUT_READY",
@@ -1684,7 +2023,7 @@ function collectRouterChecks(config, checks) {
1684
2023
  code: "ROOT_LAYOUT_MISSING",
1685
2024
  title: "Root layout is missing",
1686
2025
  message: "The application has no shared root layout.",
1687
- action: `Add ${config.srcDir}/app/layout.tsx.`
2026
+ action: `Add ${config.srcDir}/app/layout${suggestedLayoutExtension}.`
1688
2027
  });
1689
2028
  }
1690
2029
  function collectDeploymentChecks(config, userConfig, checks) {
@@ -1743,7 +2082,7 @@ function hasCronRoute(config, job) {
1743
2082
  const relative = job.path.replace(/^\/+/, "").replace(/^api\//, "");
1744
2083
  return getFarmSourceRoots(config).some((source) => {
1745
2084
  const directory = path.join(source.root, source.srcDir, "app", "api", relative);
1746
- return ROUTE_EXTENSIONS.some((extension) => existsSync(path.join(directory, `route.${extension}`)));
2085
+ return ROUTE_EXTENSIONS$1.some((extension) => existsSync(path.join(directory, `route.${extension}`)));
1747
2086
  });
1748
2087
  }
1749
2088
  function containsFile(directory, pattern) {
@@ -1811,6 +2150,415 @@ function formatCount$1(value, noun) {
1811
2150
  return `${value} ${noun}${value === 1 ? "" : "s"}`;
1812
2151
  }
1813
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
1814
2562
  //#region src/cron.ts
1815
2563
  async function loadFarmCronConfig(options = {}) {
1816
2564
  const root = path.resolve(options.root || process.cwd());
@@ -2016,7 +2764,7 @@ function resolveMigrationCommand(root, entry, index) {
2016
2764
  }
2017
2765
  function runMigrationCommand(command) {
2018
2766
  return new Promise((resolve, reject) => {
2019
- const child = spawn(command.command, {
2767
+ const child = spawn$1(command.command, {
2020
2768
  cwd: command.cwd,
2021
2769
  env: {
2022
2770
  ...process.env,
@@ -2420,6 +3168,31 @@ function toPosix(value) {
2420
3168
  return value.split(path.sep).join("/");
2421
3169
  }
2422
3170
  //#endregion
3171
+ //#region src/auth.ts
3172
+ async function migrateFarmAuth(options = {}) {
3173
+ const root = path.resolve(options.root || process.cwd());
3174
+ const userConfig = await loadConfig(root, options.configPath, "production");
3175
+ if (!userConfig) throw new Error(`No farm.config file was found in ${root}.`);
3176
+ if (!(await resolveConfig({
3177
+ ...userConfig,
3178
+ root
3179
+ }, "production")).auth.enabled) throw new Error("Farm Auth is disabled. Add `auth: true` to farm.config.ts first.");
3180
+ const resolveFromApp = createRequire(path.join(root, "package.json"));
3181
+ let modulePath;
3182
+ try {
3183
+ modulePath = resolveFromApp.resolve("@farm.js/auth/internal");
3184
+ } catch {
3185
+ throw new Error("Install @farm.js/auth before running `farm auth migrate`.");
3186
+ }
3187
+ const runtime = await import(
3188
+ /* @vite-ignore */
3189
+ pathToFileURL(modulePath).href
3190
+ );
3191
+ logger.info("Applying the Farm Auth database schema...");
3192
+ await runtime.migrateFarmAuth();
3193
+ logger.success("Farm Auth database is ready.");
3194
+ }
3195
+ //#endregion
2423
3196
  //#region src/upgrade.ts
2424
3197
  const DEPENDENCY_SECTIONS = [
2425
3198
  "dependencies",
@@ -2560,11 +3333,13 @@ function getDependencySectionFlags(packageManager, section) {
2560
3333
  }
2561
3334
  function runFarmUpgradeCommand(command) {
2562
3335
  return new Promise((resolve, reject) => {
2563
- const executable = process.platform === "win32" ? `${command.command}.cmd` : command.command;
2564
- 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, {
2565
3339
  cwd: command.cwd,
2566
3340
  env: process.env,
2567
- stdio: "inherit"
3341
+ stdio: "inherit",
3342
+ shell: isWindows
2568
3343
  });
2569
3344
  child.on("error", reject);
2570
3345
  child.on("close", (code, signal) => {
@@ -2590,6 +3365,6 @@ function formatError(error) {
2590
3365
  return error instanceof Error ? error.message : String(error);
2591
3366
  }
2592
3367
  //#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 };
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 };
2594
3369
 
2595
3370
  //# sourceMappingURL=index.mjs.map