@farm.js/cli 0.1.0-beta.14 → 0.1.0-beta.17
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/bin/farm.js +33 -2
- package/dist/index.js +681 -73
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +679 -77
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -2,13 +2,13 @@ import { n as listFarmIntegrationProviders, t as addFarmIntegration } from "./ad
|
|
|
2
2
|
import { buildFarm } from "./build.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { createServer, startDevServer } from "@farm.js/core/server";
|
|
5
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { execFileSync
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
9
|
import { existsSync as existsSync$1, readFileSync as readFileSync$1 } from "fs";
|
|
10
10
|
import path$1 from "path";
|
|
11
|
-
import { generateFarmTypeArtifacts, getFarmDocsRouteTypeEntries, getFarmSourceRoots, getIntegrationSchemas, getPresetForDeployTarget, loadConfig, logger, normalizeDeployTarget, resolveConfig, resolveDeployConfig, resolveDeployOutputPath } from "@farm.js/core";
|
|
11
|
+
import { farmRouteRuleMatches, generateFarmTypeArtifacts, getFarmDocsRouteTypeEntries, getFarmPresetRuntime, getFarmSourceRoots, getIntegrationSchemas, getPresetForDeployTarget, loadConfig, logger, mergeFarmRouteRuntimeConfigs, normalizeDeployTarget, resolveConfig, resolveDeployConfig, resolveDeployOutputPath, resolveFarmRouteRuleRuntimeConfig, resolveFarmRouteRuntimeConfig, resolveRouteRenderingConfig, scanProgrammaticPagePaths } from "@farm.js/core";
|
|
12
12
|
import { spawn, spawnSync } from "node:child_process";
|
|
13
13
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
14
14
|
import pc from "picocolors";
|
|
@@ -18,36 +18,148 @@ import { pathToFileURL } from "node:url";
|
|
|
18
18
|
var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
|
|
19
19
|
//#endregion
|
|
20
20
|
//#region src/deploy.ts
|
|
21
|
+
var FarmDeployError = class extends Error {
|
|
22
|
+
constructor(code, platform, message, options) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "FarmDeployError";
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.platform = platform;
|
|
27
|
+
this.cause = options?.cause;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
21
30
|
/**
|
|
22
31
|
* Deploy Farm.js application
|
|
23
32
|
*/
|
|
24
33
|
async function deployFarm(options = {}) {
|
|
25
|
-
const
|
|
34
|
+
const { plan, deployConfig } = await resolveFarmDeployContext(options);
|
|
35
|
+
if (options.plan) {
|
|
36
|
+
logger.info(formatFarmDeployPlan(plan));
|
|
37
|
+
return plan;
|
|
38
|
+
}
|
|
39
|
+
logger.info(`🚀 Building with ${plan.preset} preset...`);
|
|
40
|
+
await buildFarm({
|
|
41
|
+
root: plan.root,
|
|
42
|
+
preset: plan.preset
|
|
43
|
+
});
|
|
44
|
+
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.`);
|
|
45
|
+
await deployPlatform(plan.target, plan.root, plan.outputDir, deployConfig, options.prod);
|
|
46
|
+
return plan;
|
|
47
|
+
}
|
|
48
|
+
async function createFarmDeployPlan(options = {}) {
|
|
49
|
+
return (await resolveFarmDeployContext(options)).plan;
|
|
50
|
+
}
|
|
51
|
+
function formatFarmDeployPlan(plan) {
|
|
52
|
+
return [
|
|
53
|
+
"FARM / DEPLOY PLAN",
|
|
54
|
+
"",
|
|
55
|
+
`Target: ${plan.target}`,
|
|
56
|
+
`Preset: ${plan.preset}`,
|
|
57
|
+
`Runtime: ${plan.runtime}`,
|
|
58
|
+
`Output: ${plan.outputDir}`,
|
|
59
|
+
`Production: ${plan.production ? "yes" : "no"}`,
|
|
60
|
+
"",
|
|
61
|
+
`1. ${plan.build.command}`,
|
|
62
|
+
` cwd: ${plan.build.cwd}`,
|
|
63
|
+
`2. ${plan.deploy.command}`,
|
|
64
|
+
` cwd: ${plan.deploy.cwd}`,
|
|
65
|
+
...plan.cloudflareAgent ? ["", `Cloudflare Agent config: ${plan.cloudflareAgent.configPath}${plan.cloudflareAgent.generated ? " (generated during build)" : ""}`] : []
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
async function resolveFarmDeployContext(options) {
|
|
69
|
+
const root = path$1.resolve(options.root || process.cwd());
|
|
26
70
|
const mode = "production";
|
|
27
71
|
const userConfig = await loadConfig(root, void 0, mode);
|
|
28
|
-
const config =
|
|
72
|
+
const config = await resolveConfig({
|
|
73
|
+
root,
|
|
74
|
+
...userConfig
|
|
75
|
+
}, mode);
|
|
29
76
|
const cliTarget = options.vercel ? "vercel" : options.cloudflare ? "cloudflare" : options.netlify ? "netlify" : void 0;
|
|
30
|
-
const platform = normalizeDeployTarget(cliTarget || config
|
|
31
|
-
if (platform !== "vercel" && platform !== "cloudflare" && platform !== "netlify")
|
|
32
|
-
logger.error("Please specify a deployment target with --vercel, --cloudflare, --netlify, or farm.config deploy.target.");
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
77
|
+
const platform = normalizeDeployTarget(cliTarget || config.deploy.target);
|
|
78
|
+
if (platform !== "vercel" && platform !== "cloudflare" && platform !== "netlify") throw new Error("Please specify a deployment target with --vercel, --cloudflare, --netlify, or farm.config deploy.target.");
|
|
35
79
|
const deployConfig = resolveDeployConfig(userConfig || {}, {
|
|
36
80
|
target: platform,
|
|
37
81
|
preset: cliTarget ? userConfig?.deploy?.preset || userConfig?.preset || getPresetForDeployTarget(platform) : void 0
|
|
38
82
|
});
|
|
39
83
|
const preset = deployConfig.preset || getPresetForDeployTarget(platform) || "node-server";
|
|
40
|
-
logger.info(`🚀 Building with ${preset} preset...`);
|
|
41
|
-
await buildFarm({
|
|
42
|
-
root,
|
|
43
|
-
preset
|
|
44
|
-
});
|
|
45
84
|
const nitroOutput = resolveDeployOutputPath(root, deployConfig.outputDir);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
85
|
+
const cloudflareAgent = platform === "cloudflare" ? resolveCloudflareAgentDeployPlan(root) || resolveConfiguredCloudflareAgentDeployPlan(root, config.integrations) : void 0;
|
|
86
|
+
const deploy = createDeployCommand(platform, root, nitroOutput, deployConfig, options.prod, cloudflareAgent);
|
|
87
|
+
return {
|
|
88
|
+
plan: {
|
|
89
|
+
root: path$1.resolve(root),
|
|
90
|
+
target: platform,
|
|
91
|
+
preset,
|
|
92
|
+
runtime: getFarmPresetRuntime(preset),
|
|
93
|
+
outputDir: nitroOutput,
|
|
94
|
+
production: platform === "netlify" || Boolean(options.prod),
|
|
95
|
+
build: {
|
|
96
|
+
command: `farm build --preset ${preset}`,
|
|
97
|
+
cwd: path$1.resolve(root)
|
|
98
|
+
},
|
|
99
|
+
deploy,
|
|
100
|
+
...cloudflareAgent ? { cloudflareAgent } : {}
|
|
101
|
+
},
|
|
102
|
+
deployConfig
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function createDeployCommand(platform, root, outputDir, deployConfig, prod, cloudflareAgent) {
|
|
106
|
+
if (platform === "vercel") {
|
|
107
|
+
const args = [
|
|
108
|
+
"deploy",
|
|
109
|
+
"--prebuilt",
|
|
110
|
+
"--yes",
|
|
111
|
+
...prod ? ["--prod"] : []
|
|
112
|
+
];
|
|
113
|
+
return {
|
|
114
|
+
command: formatCommand$1("vercel", args),
|
|
115
|
+
cwd: path$1.resolve(root),
|
|
116
|
+
executable: "vercel",
|
|
117
|
+
args
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (platform === "netlify") {
|
|
121
|
+
const args = createNetlifyDeployArgs(deployConfig.netlify?.site);
|
|
122
|
+
return {
|
|
123
|
+
command: formatCommand$1("netlify", args),
|
|
124
|
+
cwd: outputDir,
|
|
125
|
+
executable: "netlify",
|
|
126
|
+
args
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (cloudflareAgent) {
|
|
130
|
+
const args = [
|
|
131
|
+
"deploy",
|
|
132
|
+
"--config",
|
|
133
|
+
cloudflareAgent.configPath,
|
|
134
|
+
...cloudflareAgent.environment ? ["--env", cloudflareAgent.environment] : []
|
|
135
|
+
];
|
|
136
|
+
return {
|
|
137
|
+
command: formatCommand$1("wrangler", args),
|
|
138
|
+
cwd: path$1.resolve(root),
|
|
139
|
+
executable: "wrangler",
|
|
140
|
+
args
|
|
141
|
+
};
|
|
49
142
|
}
|
|
50
|
-
|
|
143
|
+
const args = [
|
|
144
|
+
"pages",
|
|
145
|
+
"deploy",
|
|
146
|
+
".",
|
|
147
|
+
`--project-name=${deployConfig.cloudflare?.projectName || deployConfig.projectName || "farm-app"}`
|
|
148
|
+
];
|
|
149
|
+
return {
|
|
150
|
+
command: formatCommand$1("wrangler", args),
|
|
151
|
+
cwd: outputDir,
|
|
152
|
+
executable: "wrangler",
|
|
153
|
+
args
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function createNetlifyDeployArgs(site) {
|
|
157
|
+
return [
|
|
158
|
+
"deploy",
|
|
159
|
+
"--prod",
|
|
160
|
+
"--dir=.",
|
|
161
|
+
...site ? [`--site=${site}`] : []
|
|
162
|
+
];
|
|
51
163
|
}
|
|
52
164
|
/**
|
|
53
165
|
* Deploy using platform's native CLI (user credentials)
|
|
@@ -69,19 +181,20 @@ async function deployPlatform(platform, root, outputDir, deployConfig, prod) {
|
|
|
69
181
|
async function deployVercel(root, outputDir, prod) {
|
|
70
182
|
logger.info("🚀 Deploying to Vercel...");
|
|
71
183
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
184
|
+
execFileSync("vercel", ["--version"], {
|
|
185
|
+
stdio: "ignore",
|
|
186
|
+
cwd: root
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw new FarmDeployError("CLI_NOT_INSTALLED", "vercel", "Vercel CLI is not installed. Install it with: npm i -g vercel", { cause: error });
|
|
77
190
|
}
|
|
78
191
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
192
|
+
execFileSync("vercel", ["whoami"], {
|
|
193
|
+
stdio: "ignore",
|
|
194
|
+
cwd: root
|
|
195
|
+
});
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw new FarmDeployError("CLI_NOT_AUTHENTICATED", "vercel", "Vercel CLI is not authenticated. Run 'vercel login', then retry the deployment.", { cause: error });
|
|
85
198
|
}
|
|
86
199
|
try {
|
|
87
200
|
const { existsSync, statSync, readdirSync } = await import("fs");
|
|
@@ -90,14 +203,8 @@ async function deployVercel(root, outputDir, prod) {
|
|
|
90
203
|
const configFile = path$1.join(outputDir, "config.json");
|
|
91
204
|
const serverIndex = path$1.join(functionsDir, "index.mjs");
|
|
92
205
|
logger.info("🔍 Verifying deployment structure...");
|
|
93
|
-
if (!existsSync(functionsDir)) {
|
|
94
|
-
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
if (!existsSync(serverIndex)) {
|
|
98
|
-
logger.error(`❌ Server entry point not found at ${serverIndex}`);
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
206
|
+
if (!existsSync(functionsDir)) throw new FarmDeployError("INVALID_BUILD_OUTPUT", "vercel", `Functions directory not found at ${functionsDir}.`);
|
|
207
|
+
if (!existsSync(serverIndex)) throw new FarmDeployError("INVALID_BUILD_OUTPUT", "vercel", `Server entry point not found at ${serverIndex}.`);
|
|
101
208
|
logger.info(`✅ Functions directory: ${functionsDir}`);
|
|
102
209
|
if (!existsSync(staticDir)) logger.warn(`⚠️ Static directory not found at ${staticDir}`);
|
|
103
210
|
else {
|
|
@@ -178,14 +285,19 @@ async function deployVercel(root, outputDir, prod) {
|
|
|
178
285
|
logger.info(` Static: ${staticDir}`);
|
|
179
286
|
logger.info(` Config: ${configFile}`);
|
|
180
287
|
logger.info("📤 Uploading to Vercel...");
|
|
181
|
-
|
|
288
|
+
execFileSync("vercel", [
|
|
289
|
+
"deploy",
|
|
290
|
+
"--prebuilt",
|
|
291
|
+
"--yes",
|
|
292
|
+
...prod ? ["--prod"] : []
|
|
293
|
+
], {
|
|
182
294
|
stdio: "inherit",
|
|
183
295
|
cwd: root
|
|
184
296
|
});
|
|
185
297
|
logger.success("✅ Deployed to Vercel successfully!");
|
|
186
298
|
} catch (error) {
|
|
187
|
-
|
|
188
|
-
|
|
299
|
+
if (error instanceof FarmDeployError) throw error;
|
|
300
|
+
throw new FarmDeployError("DEPLOY_FAILED", "vercel", `Failed to deploy to Vercel: ${getErrorMessage(error)}`, { cause: error });
|
|
189
301
|
}
|
|
190
302
|
}
|
|
191
303
|
/** Deploy to a composed Worker or fall back to Cloudflare Pages. */
|
|
@@ -206,8 +318,7 @@ async function deployCloudflare(root, outputDir, projectName) {
|
|
|
206
318
|
});
|
|
207
319
|
logger.success("✅ Deployed Farm and Cloudflare Agents successfully!");
|
|
208
320
|
} catch (error) {
|
|
209
|
-
|
|
210
|
-
process.exit(1);
|
|
321
|
+
throw new FarmDeployError("DEPLOY_FAILED", "cloudflare", `Failed to deploy to Cloudflare: ${getErrorMessage(error)}`, { cause: error });
|
|
211
322
|
}
|
|
212
323
|
return;
|
|
213
324
|
}
|
|
@@ -225,8 +336,7 @@ async function deployCloudflare(root, outputDir, projectName) {
|
|
|
225
336
|
});
|
|
226
337
|
logger.success("✅ Deployed to Cloudflare Pages successfully!");
|
|
227
338
|
} catch (error) {
|
|
228
|
-
|
|
229
|
-
process.exit(1);
|
|
339
|
+
throw new FarmDeployError("DEPLOY_FAILED", "cloudflare", `Failed to deploy to Cloudflare: ${getErrorMessage(error)}`, { cause: error });
|
|
230
340
|
}
|
|
231
341
|
}
|
|
232
342
|
/** Read the trusted Workers deployment handoff emitted by @farm.js/cf-agent. */
|
|
@@ -252,16 +362,34 @@ function resolveCloudflareAgentDeployPlan(root) {
|
|
|
252
362
|
...typeof environment === "string" ? { environment: environment.trim() } : {}
|
|
253
363
|
};
|
|
254
364
|
}
|
|
365
|
+
function resolveConfiguredCloudflareAgentDeployPlan(root, integrations) {
|
|
366
|
+
const integration = Object.values(integrations || {}).find((value) => isRecord(value) && value.category === "agent" && value.type === "cloudflare" && value.serverRuntime === false);
|
|
367
|
+
if (!isRecord(integration) || !isRecord(integration.instance)) return void 0;
|
|
368
|
+
const configuredPath = integration.instance.config;
|
|
369
|
+
if (typeof configuredPath !== "string" || !configuredPath.trim()) return void 0;
|
|
370
|
+
const projectRoot = path$1.resolve(root);
|
|
371
|
+
const sourceConfigPath = path$1.resolve(projectRoot, configuredPath);
|
|
372
|
+
assertPathInsideProject(projectRoot, sourceConfigPath, "Cloudflare Agents source config");
|
|
373
|
+
const configPath = path$1.join(path$1.dirname(sourceConfigPath), ".farm-cf-agent.wrangler.jsonc");
|
|
374
|
+
const environment = integration.instance.environment;
|
|
375
|
+
return {
|
|
376
|
+
configPath,
|
|
377
|
+
...typeof environment === "string" && environment.trim() ? { environment: environment.trim() } : {},
|
|
378
|
+
generated: true
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function assertPathInsideProject(projectRoot, candidate, label) {
|
|
382
|
+
const relativePath = path$1.relative(projectRoot, candidate);
|
|
383
|
+
if (relativePath === ".." || relativePath.startsWith(`..${path$1.sep}`) || path$1.isAbsolute(relativePath)) throw new Error(`${label} must stay inside the Farm project root.`);
|
|
384
|
+
}
|
|
255
385
|
function assertWranglerInstalled(root) {
|
|
256
386
|
try {
|
|
257
387
|
execFileSync("wrangler", ["--version"], {
|
|
258
388
|
stdio: "ignore",
|
|
259
389
|
cwd: root
|
|
260
390
|
});
|
|
261
|
-
} catch {
|
|
262
|
-
|
|
263
|
-
logger.info("💡 Install it in this project with: npm i -D wrangler");
|
|
264
|
-
process.exit(1);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
throw new FarmDeployError("CLI_NOT_INSTALLED", "cloudflare", "Wrangler CLI is not installed. Install it in this project with: npm i -D wrangler", { cause: error });
|
|
265
393
|
}
|
|
266
394
|
}
|
|
267
395
|
function isRecord(value) {
|
|
@@ -273,22 +401,33 @@ function isRecord(value) {
|
|
|
273
401
|
async function deployNetlify(root, outputDir, site) {
|
|
274
402
|
logger.info("🚀 Deploying to Netlify...");
|
|
275
403
|
try {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
404
|
+
execFileSync("netlify", ["--version"], {
|
|
405
|
+
stdio: "ignore",
|
|
406
|
+
cwd: root
|
|
407
|
+
});
|
|
408
|
+
} catch (error) {
|
|
409
|
+
throw new FarmDeployError("CLI_NOT_INSTALLED", "netlify", "Netlify CLI is not installed. Install it with: npm i -g netlify-cli", { cause: error });
|
|
281
410
|
}
|
|
282
411
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
412
|
+
execFileSync("netlify", createNetlifyDeployArgs(site), {
|
|
413
|
+
stdio: "inherit",
|
|
414
|
+
cwd: outputDir
|
|
415
|
+
});
|
|
286
416
|
logger.success("✅ Deployed to Netlify successfully!");
|
|
287
417
|
} catch (error) {
|
|
288
|
-
|
|
289
|
-
process.exit(1);
|
|
418
|
+
throw new FarmDeployError("DEPLOY_FAILED", "netlify", `Failed to deploy to Netlify: ${getErrorMessage(error)}`, { cause: error });
|
|
290
419
|
}
|
|
291
420
|
}
|
|
421
|
+
function formatCommand$1(executable, args) {
|
|
422
|
+
return [executable, ...args].map(formatCommandArgument).join(" ");
|
|
423
|
+
}
|
|
424
|
+
function formatCommandArgument(argument) {
|
|
425
|
+
if (/^[A-Za-z0-9_./:=@+-]+$/.test(argument)) return argument;
|
|
426
|
+
return `'${argument.replace(/'/g, `'"'"'`)}'`;
|
|
427
|
+
}
|
|
428
|
+
function getErrorMessage(error) {
|
|
429
|
+
return error instanceof Error ? error.message : String(error);
|
|
430
|
+
}
|
|
292
431
|
//#endregion
|
|
293
432
|
//#region src/preview-gateway.ts
|
|
294
433
|
const DEFAULT_GATEWAY_URL = "https://preview.farming-labs.dev";
|
|
@@ -800,10 +939,20 @@ function normalizePreviewDomain(value) {
|
|
|
800
939
|
}
|
|
801
940
|
//#endregion
|
|
802
941
|
//#region src/generate.ts
|
|
942
|
+
var FarmGeneratedArtifactsStaleError = class extends Error {
|
|
943
|
+
constructor(root, stalePaths) {
|
|
944
|
+
const normalizedPaths = [...new Set(stalePaths)].sort();
|
|
945
|
+
const relativePaths = normalizedPaths.map((filePath) => path.relative(root, filePath));
|
|
946
|
+
super(`Generated types are stale:\n${relativePaths.map((filePath) => ` - ${filePath}`).join("\n")}\nRun farm generate and commit the updated files.`);
|
|
947
|
+
this.name = "FarmGeneratedArtifactsStaleError";
|
|
948
|
+
this.stalePaths = normalizedPaths;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
803
951
|
const PRISMA_GENERATED_START = "// Farm.js integrations generated schema: start";
|
|
804
952
|
const PRISMA_GENERATED_END = "// Farm.js integrations generated schema: end";
|
|
805
953
|
async function generateFarmArtifacts(options = {}) {
|
|
806
954
|
const root = path.resolve(options.root || process.cwd());
|
|
955
|
+
if (options.check && hasSchemaOptions(options)) throw new Error("--check verifies generated framework types and cannot be combined with schema output options.");
|
|
807
956
|
const userConfig = await loadConfig(root, options.configPath, "development");
|
|
808
957
|
if (!userConfig && hasSchemaOptions(options)) throw new Error("No Farm config found. Please create farm.config.ts or config.ts.");
|
|
809
958
|
const resolvedConfig = await resolveConfig({
|
|
@@ -818,14 +967,20 @@ async function generateFarmArtifacts(options = {}) {
|
|
|
818
967
|
layers: resolvedConfig.layers,
|
|
819
968
|
extraRoutes,
|
|
820
969
|
suppressLintOnLink: resolvedConfig.suppressLintOnLink,
|
|
821
|
-
i18nConfig: resolvedConfig.i18n
|
|
970
|
+
i18nConfig: resolvedConfig.i18n,
|
|
971
|
+
check: options.check
|
|
822
972
|
});
|
|
973
|
+
if (options.check) {
|
|
974
|
+
if (typeArtifacts.stalePaths.length) throw new FarmGeneratedArtifactsStaleError(root, typeArtifacts.stalePaths);
|
|
975
|
+
logger.success(`Generated route, API, env${resolvedConfig.i18n.enabled ? ", and i18n" : ""} types are up to date.`);
|
|
976
|
+
return typeArtifacts;
|
|
977
|
+
}
|
|
823
978
|
logger.success(`Generated route, API, env${resolvedConfig.i18n.enabled ? ", and i18n" : ""} types (${typeArtifacts.apiRoutes.length} API route${typeArtifacts.apiRoutes.length === 1 ? "" : "s"}).`);
|
|
824
979
|
const schemas = getIntegrationSchemas(resolvedConfig.integrations);
|
|
825
980
|
const schemaEntries = Object.entries(schemas);
|
|
826
981
|
if (!schemaEntries.length) {
|
|
827
982
|
if (hasSchemaOptions(options)) logger.warn("No integration schemas were found in the current Farm config.");
|
|
828
|
-
return;
|
|
983
|
+
return typeArtifacts;
|
|
829
984
|
}
|
|
830
985
|
const packageManifest = await readPackageManifest(root);
|
|
831
986
|
const schemaOptionsExplicit = hasSchemaOptions(options);
|
|
@@ -835,12 +990,12 @@ async function generateFarmArtifacts(options = {}) {
|
|
|
835
990
|
} catch (error) {
|
|
836
991
|
if (schemaOptionsExplicit) throw error;
|
|
837
992
|
logger.warn(`Integration schemas were found, but Farm could not choose a schema target automatically: ${error.message}`);
|
|
838
|
-
return;
|
|
993
|
+
return typeArtifacts;
|
|
839
994
|
}
|
|
840
995
|
if (!orm) {
|
|
841
996
|
if (!schemaOptionsExplicit) {
|
|
842
997
|
logger.warn("Integration schemas were found, but no data layer was detected. Pass --orm prisma|drizzle|postgres|mysql|sqlite|mongodb to generate schema artifacts.");
|
|
843
|
-
return;
|
|
998
|
+
return typeArtifacts;
|
|
844
999
|
}
|
|
845
1000
|
throw new Error("Could not auto-detect a schema target. Pass one explicitly with --orm prisma|drizzle|postgres|mysql|sqlite|mongodb.");
|
|
846
1001
|
}
|
|
@@ -851,7 +1006,7 @@ async function generateFarmArtifacts(options = {}) {
|
|
|
851
1006
|
if (!existsSync(schemaPath)) throw new Error(`Prisma target was selected but no schema file was found at ${schemaPath}. Create prisma/schema.prisma or pass --output.`);
|
|
852
1007
|
await writePrismaSchema(schemaPath, collectedModels);
|
|
853
1008
|
logger.success(`Generated Prisma integration schema in ${path.relative(root, schemaPath)}.`);
|
|
854
|
-
return;
|
|
1009
|
+
return typeArtifacts;
|
|
855
1010
|
}
|
|
856
1011
|
case "drizzle": {
|
|
857
1012
|
const dialect = options.dialect ?? await detectDrizzleDialect(root, packageManifest) ?? void 0;
|
|
@@ -859,7 +1014,7 @@ async function generateFarmArtifacts(options = {}) {
|
|
|
859
1014
|
const outputPath = options.output ? path.resolve(root, options.output) : path.join(root, "farm-integrations.generated.ts");
|
|
860
1015
|
await writeGeneratedFile(outputPath, generateDrizzleSchema(collectedModels, dialect));
|
|
861
1016
|
logger.success(`Generated Drizzle integration schema in ${path.relative(root, outputPath)}.`);
|
|
862
|
-
return;
|
|
1017
|
+
return typeArtifacts;
|
|
863
1018
|
}
|
|
864
1019
|
case "postgres":
|
|
865
1020
|
case "mysql":
|
|
@@ -867,13 +1022,13 @@ async function generateFarmArtifacts(options = {}) {
|
|
|
867
1022
|
const outputPath = options.output ? path.resolve(root, options.output) : path.join(root, `farm-integrations.generated.${orm}.sql`);
|
|
868
1023
|
await writeGeneratedFile(outputPath, generateSqlSchema(collectedModels, orm));
|
|
869
1024
|
logger.success(`Generated ${orm} integration schema in ${path.relative(root, outputPath)}.`);
|
|
870
|
-
return;
|
|
1025
|
+
return typeArtifacts;
|
|
871
1026
|
}
|
|
872
1027
|
case "mongodb": {
|
|
873
1028
|
const outputPath = options.output ? path.resolve(root, options.output) : path.join(root, "farm-integrations.generated.mongodb.ts");
|
|
874
1029
|
await writeGeneratedFile(outputPath, generateMongoBootstrap(collectedModels));
|
|
875
1030
|
logger.success(`Generated MongoDB integration bootstrap in ${path.relative(root, outputPath)}.`);
|
|
876
|
-
return;
|
|
1031
|
+
return typeArtifacts;
|
|
877
1032
|
}
|
|
878
1033
|
}
|
|
879
1034
|
}
|
|
@@ -1058,7 +1213,7 @@ function cloneSchemaModel(model) {
|
|
|
1058
1213
|
async function writePrismaSchema(schemaPath, models) {
|
|
1059
1214
|
const source = await readFile(schemaPath, "utf8");
|
|
1060
1215
|
const generated = createPrismaGeneratedBlock(generatePrismaSchema(models));
|
|
1061
|
-
const pattern = new RegExp(`${escapeRegExp(PRISMA_GENERATED_START)}[\\s\\S]*?${escapeRegExp(PRISMA_GENERATED_END)}`, "m");
|
|
1216
|
+
const pattern = new RegExp(`${escapeRegExp$1(PRISMA_GENERATED_START)}[\\s\\S]*?${escapeRegExp$1(PRISMA_GENERATED_END)}`, "m");
|
|
1062
1217
|
const nextSource = pattern.test(source) ? source.replace(pattern, generated) : `${source.trimEnd()}\n\n${generated}\n`;
|
|
1063
1218
|
await writeFile(schemaPath, nextSource, "utf8");
|
|
1064
1219
|
}
|
|
@@ -1386,12 +1541,12 @@ function toCamelCase(value) {
|
|
|
1386
1541
|
function escapeString(value) {
|
|
1387
1542
|
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/'/g, "''");
|
|
1388
1543
|
}
|
|
1389
|
-
function escapeRegExp(value) {
|
|
1544
|
+
function escapeRegExp$1(value) {
|
|
1390
1545
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1391
1546
|
}
|
|
1392
1547
|
//#endregion
|
|
1393
1548
|
//#region src/doctor.ts
|
|
1394
|
-
const ROUTE_EXTENSIONS = [
|
|
1549
|
+
const ROUTE_EXTENSIONS$1 = [
|
|
1395
1550
|
"ts",
|
|
1396
1551
|
"tsx",
|
|
1397
1552
|
"js",
|
|
@@ -1413,7 +1568,7 @@ async function runFarmDoctor(options = {}) {
|
|
|
1413
1568
|
const root = path.resolve(options.root || process.cwd());
|
|
1414
1569
|
const liveTarget = resolveLiveTarget(options);
|
|
1415
1570
|
let liveError;
|
|
1416
|
-
if (!options.offline) try {
|
|
1571
|
+
if (!options.offline && !options.fix) try {
|
|
1417
1572
|
return createLiveReport(await fetchLiveSnapshot(liveTarget, options), liveTarget, options.now);
|
|
1418
1573
|
} catch (error) {
|
|
1419
1574
|
liveError = formatError$2(error);
|
|
@@ -1460,6 +1615,13 @@ function formatFarmDoctorReport(report, options = {}) {
|
|
|
1460
1615
|
`${report.summary.fail} failed`,
|
|
1461
1616
|
`${report.summary.info} info`
|
|
1462
1617
|
].join(" / ");
|
|
1618
|
+
if (report.fixes?.length) {
|
|
1619
|
+
lines.push("", color.bold("FIXED"));
|
|
1620
|
+
for (const fix of report.fixes) {
|
|
1621
|
+
lines.push(` ${color.green("✓")} ${fix.title}`);
|
|
1622
|
+
lines.push(` ${color.dim(fix.filePath)}`);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1463
1625
|
lines.push("", `${color.bold("SUMMARY")} ${summary}`);
|
|
1464
1626
|
if (report.target?.devtoolsUrl) lines.push(`${color.bold("DEVTOOLS")} ${report.target.devtoolsUrl}`);
|
|
1465
1627
|
return lines.join("\n");
|
|
@@ -1559,9 +1721,10 @@ async function createProjectReport(root, options) {
|
|
|
1559
1721
|
action: "Add farm.config.ts and export defineConfig({...})."
|
|
1560
1722
|
});
|
|
1561
1723
|
else {
|
|
1724
|
+
const configRoot = path.resolve(root, userConfig.root || ".");
|
|
1562
1725
|
config = await resolveConfig({
|
|
1563
|
-
|
|
1564
|
-
|
|
1726
|
+
...userConfig,
|
|
1727
|
+
root: configRoot
|
|
1565
1728
|
}, "development");
|
|
1566
1729
|
const configFile = findConfigFile(root, options.configPath);
|
|
1567
1730
|
checks.push({
|
|
@@ -1585,9 +1748,40 @@ async function createProjectReport(root, options) {
|
|
|
1585
1748
|
report.target = collectDeploymentChecks(config, userConfig, checks);
|
|
1586
1749
|
collectCronChecks(config, options.env || process.env, checks);
|
|
1587
1750
|
}
|
|
1751
|
+
if (config && options.fix) {
|
|
1752
|
+
const fixes = applySafeProjectFixes(root, config, checks);
|
|
1753
|
+
if (fixes.length) {
|
|
1754
|
+
const refreshed = await createProjectReport(root, {
|
|
1755
|
+
...options,
|
|
1756
|
+
fix: false
|
|
1757
|
+
});
|
|
1758
|
+
refreshed.fixes = fixes;
|
|
1759
|
+
return refreshed;
|
|
1760
|
+
}
|
|
1761
|
+
report.fixes = [];
|
|
1762
|
+
}
|
|
1588
1763
|
finalizeReport(report);
|
|
1589
1764
|
return report;
|
|
1590
1765
|
}
|
|
1766
|
+
function applySafeProjectFixes(root, config, checks) {
|
|
1767
|
+
const fixes = [];
|
|
1768
|
+
if (checks.some((check) => check.code === "ROOT_LAYOUT_MISSING")) {
|
|
1769
|
+
const layoutPath = path.join(config.root, config.srcDir, "app", "layout.tsx");
|
|
1770
|
+
if (!existsSync(layoutPath)) {
|
|
1771
|
+
mkdirSync(path.dirname(layoutPath), { recursive: true });
|
|
1772
|
+
writeFileSync(layoutPath, `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`, {
|
|
1773
|
+
encoding: "utf8",
|
|
1774
|
+
flag: "wx"
|
|
1775
|
+
});
|
|
1776
|
+
fixes.push({
|
|
1777
|
+
code: "ROOT_LAYOUT_CREATED",
|
|
1778
|
+
title: "Created the missing root layout",
|
|
1779
|
+
filePath: path.relative(root, layoutPath)
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return fixes;
|
|
1784
|
+
}
|
|
1591
1785
|
function collectNodeCheck(checks) {
|
|
1592
1786
|
const major = Number(process.versions.node.split(".")[0]);
|
|
1593
1787
|
checks.push(major >= 18 ? {
|
|
@@ -1648,7 +1842,7 @@ function collectRouterChecks(config, checks) {
|
|
|
1648
1842
|
const sources = getFarmSourceRoots(config);
|
|
1649
1843
|
const appDirectories = sources.map((source) => path.join(source.root, source.srcDir, "app"));
|
|
1650
1844
|
const hasPages = appDirectories.some((directory) => containsFile(directory, /^page\.(?:ts|tsx|js|jsx|md|mdx)$/));
|
|
1651
|
-
const hasProgrammaticRoutes = sources.some((source) => ROUTE_EXTENSIONS.some((extension) => existsSync(path.join(source.root, source.srcDir, `farm.routes.${extension}`))));
|
|
1845
|
+
const hasProgrammaticRoutes = sources.some((source) => ROUTE_EXTENSIONS$1.some((extension) => existsSync(path.join(source.root, source.srcDir, `farm.routes.${extension}`))));
|
|
1652
1846
|
checks.push(hasPages || hasProgrammaticRoutes ? {
|
|
1653
1847
|
status: "pass",
|
|
1654
1848
|
code: "APP_ROUTER_READY",
|
|
@@ -1661,7 +1855,7 @@ function collectRouterChecks(config, checks) {
|
|
|
1661
1855
|
message: `Farm found no page modules under ${config.srcDir}/app.`,
|
|
1662
1856
|
action: `Add ${config.srcDir}/app/page.tsx or ${config.srcDir}/farm.routes.tsx.`
|
|
1663
1857
|
});
|
|
1664
|
-
const hasRootLayout = appDirectories.some((directory) => ROUTE_EXTENSIONS.some((extension) => existsSync(path.join(directory, `layout.${extension}`))));
|
|
1858
|
+
const hasRootLayout = appDirectories.some((directory) => ROUTE_EXTENSIONS$1.some((extension) => existsSync(path.join(directory, `layout.${extension}`))));
|
|
1665
1859
|
checks.push(hasRootLayout ? {
|
|
1666
1860
|
status: "pass",
|
|
1667
1861
|
code: "ROOT_LAYOUT_READY",
|
|
@@ -1731,7 +1925,7 @@ function hasCronRoute(config, job) {
|
|
|
1731
1925
|
const relative = job.path.replace(/^\/+/, "").replace(/^api\//, "");
|
|
1732
1926
|
return getFarmSourceRoots(config).some((source) => {
|
|
1733
1927
|
const directory = path.join(source.root, source.srcDir, "app", "api", relative);
|
|
1734
|
-
return ROUTE_EXTENSIONS.some((extension) => existsSync(path.join(directory, `route.${extension}`)));
|
|
1928
|
+
return ROUTE_EXTENSIONS$1.some((extension) => existsSync(path.join(directory, `route.${extension}`)));
|
|
1735
1929
|
});
|
|
1736
1930
|
}
|
|
1737
1931
|
function containsFile(directory, pattern) {
|
|
@@ -1799,6 +1993,414 @@ function formatCount$1(value, noun) {
|
|
|
1799
1993
|
return `${value} ${noun}${value === 1 ? "" : "s"}`;
|
|
1800
1994
|
}
|
|
1801
1995
|
//#endregion
|
|
1996
|
+
//#region src/explain.ts
|
|
1997
|
+
const ROUTE_EXTENSIONS = [
|
|
1998
|
+
"tsx",
|
|
1999
|
+
"ts",
|
|
2000
|
+
"jsx",
|
|
2001
|
+
"js",
|
|
2002
|
+
"mdx",
|
|
2003
|
+
"md"
|
|
2004
|
+
];
|
|
2005
|
+
const MIDDLEWARE_EXTENSIONS = [
|
|
2006
|
+
"ts",
|
|
2007
|
+
"tsx",
|
|
2008
|
+
"js",
|
|
2009
|
+
"jsx",
|
|
2010
|
+
"mjs",
|
|
2011
|
+
"cjs"
|
|
2012
|
+
];
|
|
2013
|
+
const SOCIAL_IMAGE_EXTENSIONS = [
|
|
2014
|
+
"tsx",
|
|
2015
|
+
"ts",
|
|
2016
|
+
"jsx",
|
|
2017
|
+
"js",
|
|
2018
|
+
"png",
|
|
2019
|
+
"jpg",
|
|
2020
|
+
"jpeg",
|
|
2021
|
+
"gif",
|
|
2022
|
+
"webp"
|
|
2023
|
+
];
|
|
2024
|
+
async function explainFarmRoute(pathname, options = {}) {
|
|
2025
|
+
const root = path.resolve(options.root || process.cwd());
|
|
2026
|
+
const userConfig = await loadConfig(root, options.configPath, "production");
|
|
2027
|
+
const config = await resolveConfig({
|
|
2028
|
+
root,
|
|
2029
|
+
...userConfig
|
|
2030
|
+
}, "production");
|
|
2031
|
+
const normalizedPathname = normalizePathname(pathname, config.basePath || "/");
|
|
2032
|
+
const page = discoverMatchingPages(config, normalizedPathname).sort((left, right) => right.score - left.score || right.priority - left.priority)[0];
|
|
2033
|
+
if (!page) throw new Error(`No Farm page route matches ${normalizedPathname}.`);
|
|
2034
|
+
const layouts = collectInheritedRouteFiles(config, normalizedPathname, "layout", ROUTE_EXTENSIONS);
|
|
2035
|
+
const middleware = collectMiddleware(root, Boolean(userConfig?.middleware && Object.keys(userConfig.middleware).length), config, normalizedPathname);
|
|
2036
|
+
const pageSource = readFileSync(page.filePath, "utf8");
|
|
2037
|
+
const layoutSources = layouts.map((filePath) => ({
|
|
2038
|
+
filePath,
|
|
2039
|
+
source: readFileSync(filePath, "utf8")
|
|
2040
|
+
}));
|
|
2041
|
+
const runtime = resolveFarmRouteRuntimeConfig(mergeFarmRouteRuntimeConfigs(resolveFarmRouteRuleRuntimeConfig(normalizedPathname, config.routeRules), ...layoutSources.map(({ source }) => readRuntimeExports(source)), readRuntimeExports(pageSource)), `Route ${page.pattern}`);
|
|
2042
|
+
const matchingRules = Object.entries(config.routeRules).filter(([pattern]) => farmRouteRuleMatches(pattern, normalizedPathname)).sort(([left], [right]) => routeSpecificity(left) - routeSpecificity(right));
|
|
2043
|
+
const rendering = resolveRendering(pageSource, matchingRules);
|
|
2044
|
+
const cache = resolveCaching(pageSource, matchingRules);
|
|
2045
|
+
const metadataSources = [...layoutSources, {
|
|
2046
|
+
filePath: page.filePath,
|
|
2047
|
+
source: pageSource
|
|
2048
|
+
}];
|
|
2049
|
+
const openGraphImage = findNearestSocialImage(config, normalizedPathname, "opengraph-image");
|
|
2050
|
+
const twitterImage = findNearestSocialImage(config, normalizedPathname, "twitter-image");
|
|
2051
|
+
const preset = String(config.deploy.preset || config.preset || "node-server");
|
|
2052
|
+
const presetRuntime = getFarmPresetRuntime(preset);
|
|
2053
|
+
const compatible = rendering.mode === "static" || rendering.mode === "client" || runtime.runtime === "auto" || presetRuntime !== "unknown" && runtime.runtime === presetRuntime;
|
|
2054
|
+
const warnings = [];
|
|
2055
|
+
if (presetRuntime === "unknown" && runtime.runtime !== "auto") warnings.push(`Farm cannot verify the ${runtime.runtime} route requirement because the ${preset} preset runtime is unknown.`);
|
|
2056
|
+
else if (!compatible) warnings.push(`The route requires ${runtime.runtime}, but the ${preset} preset emits ${presetRuntime} functions.`);
|
|
2057
|
+
if (runtime.regions?.length && preset !== "vercel" && preset !== "vercel-edge") warnings.push(`${preset} does not map Farm per-route region hints.`);
|
|
2058
|
+
if (runtime.maxDuration && preset !== "vercel") warnings.push(`${preset} does not map Farm per-route maxDuration.`);
|
|
2059
|
+
return {
|
|
2060
|
+
pathname: normalizedPathname,
|
|
2061
|
+
pattern: page.pattern,
|
|
2062
|
+
params: page.params,
|
|
2063
|
+
filePath: toProjectPath(root, page.filePath),
|
|
2064
|
+
source: page.source,
|
|
2065
|
+
layouts: layouts.map((filePath) => toProjectPath(root, filePath)),
|
|
2066
|
+
middleware,
|
|
2067
|
+
runtime,
|
|
2068
|
+
rendering,
|
|
2069
|
+
cache,
|
|
2070
|
+
metadata: {
|
|
2071
|
+
static: metadataSources.filter(({ source }) => /export\s+const\s+metadata\b/.test(source)).map(({ filePath }) => toProjectPath(root, filePath)),
|
|
2072
|
+
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)),
|
|
2073
|
+
...openGraphImage ? { openGraphImage: toProjectPath(root, openGraphImage) } : {},
|
|
2074
|
+
...twitterImage ? { twitterImage: toProjectPath(root, twitterImage) } : {}
|
|
2075
|
+
},
|
|
2076
|
+
deployment: {
|
|
2077
|
+
target: String(config.deploy.target || "node"),
|
|
2078
|
+
preset,
|
|
2079
|
+
runtime: presetRuntime,
|
|
2080
|
+
compatible,
|
|
2081
|
+
warnings
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
function formatFarmRouteExplanation(explanation, options = {}) {
|
|
2086
|
+
const color = options.color === void 0 ? pc : pc.createColors(options.color);
|
|
2087
|
+
const lines = [
|
|
2088
|
+
color.bold("FARM / EXPLAIN"),
|
|
2089
|
+
"",
|
|
2090
|
+
`${color.bold("Path")} ${explanation.pathname}`,
|
|
2091
|
+
`${color.bold("Pattern")} ${explanation.pattern}`,
|
|
2092
|
+
`${color.bold("File")} ${explanation.filePath}`,
|
|
2093
|
+
`${color.bold("Source")} ${explanation.source}`,
|
|
2094
|
+
`${color.bold("Params")} ${formatParams(explanation.params)}`,
|
|
2095
|
+
`${color.bold("Layouts")} ${explanation.layouts.length ? explanation.layouts.join(" -> ") : "none"}`,
|
|
2096
|
+
`${color.bold("Middleware")} ${explanation.middleware.length ? explanation.middleware.map((entry) => entry.filePath).join(", ") : "none"}`,
|
|
2097
|
+
`${color.bold("Runtime")} ${formatRuntime(explanation.runtime)}`,
|
|
2098
|
+
`${color.bold("Rendering")} ${explanation.rendering.mode} (${explanation.rendering.reason})${explanation.rendering.ppr ? ", PPR" : ""}`,
|
|
2099
|
+
`${color.bold("Caching")} ${formatCaching(explanation.cache)}`,
|
|
2100
|
+
`${color.bold("Metadata")} ${formatMetadata(explanation.metadata)}`,
|
|
2101
|
+
`${color.bold("Deployment")} ${explanation.deployment.target} / ${explanation.deployment.preset} — ${explanation.deployment.compatible ? color.green("compatible") : color.red("incompatible")}`
|
|
2102
|
+
];
|
|
2103
|
+
for (const warning of explanation.deployment.warnings) lines.push(` ${color.yellow("!")} ${warning}`);
|
|
2104
|
+
return lines.join("\n");
|
|
2105
|
+
}
|
|
2106
|
+
function discoverMatchingPages(config, pathname) {
|
|
2107
|
+
const candidates = [];
|
|
2108
|
+
for (const [priority, source] of getFarmSourceRoots(config).entries()) {
|
|
2109
|
+
const appDirectory = path.join(source.root, source.srcDir, "app");
|
|
2110
|
+
if (existsSync(appDirectory)) for (const filePath of walkFiles(appDirectory)) {
|
|
2111
|
+
if (!/^page\.(?:tsx?|jsx?|mdx?)$/.test(path.basename(filePath))) continue;
|
|
2112
|
+
const relativeDirectory = path.relative(appDirectory, path.dirname(filePath));
|
|
2113
|
+
if (relativeDirectory.split(path.sep).includes("api") || isRouteSlotDirectory(relativeDirectory)) continue;
|
|
2114
|
+
const pattern = directoryToRoutePattern(relativeDirectory);
|
|
2115
|
+
const match = matchRoutePattern(pattern, pathname);
|
|
2116
|
+
if (!match) continue;
|
|
2117
|
+
candidates.push({
|
|
2118
|
+
filePath,
|
|
2119
|
+
pattern,
|
|
2120
|
+
params: match.params,
|
|
2121
|
+
score: match.score,
|
|
2122
|
+
source: source.name,
|
|
2123
|
+
priority
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
const sourceDirectory = path.join(source.root, source.srcDir);
|
|
2127
|
+
if (!existsSync(sourceDirectory)) continue;
|
|
2128
|
+
for (const filePath of walkFiles(sourceDirectory)) {
|
|
2129
|
+
if (!/\.(?:tsx?|jsx?)$/.test(filePath) || filePath.endsWith(".d.ts")) continue;
|
|
2130
|
+
const moduleSource = readFileSync(filePath, "utf8");
|
|
2131
|
+
for (const pattern of scanProgrammaticPagePaths(moduleSource)) {
|
|
2132
|
+
const match = matchRoutePattern(pattern, pathname);
|
|
2133
|
+
if (!match) continue;
|
|
2134
|
+
candidates.push({
|
|
2135
|
+
filePath,
|
|
2136
|
+
pattern,
|
|
2137
|
+
params: match.params,
|
|
2138
|
+
score: match.score,
|
|
2139
|
+
source: source.name,
|
|
2140
|
+
priority
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return candidates;
|
|
2146
|
+
}
|
|
2147
|
+
function isRouteSlotDirectory(relativeDirectory) {
|
|
2148
|
+
return relativeDirectory.split(path.sep).some((segment) => /^@[A-Za-z][\w-]*$/.test(segment));
|
|
2149
|
+
}
|
|
2150
|
+
function walkFiles(directory) {
|
|
2151
|
+
const files = [];
|
|
2152
|
+
const pending = [directory];
|
|
2153
|
+
while (pending.length) {
|
|
2154
|
+
const current = pending.pop();
|
|
2155
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
2156
|
+
const entryPath = path.join(current, entry.name);
|
|
2157
|
+
if (entry.isDirectory()) {
|
|
2158
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
2159
|
+
pending.push(entryPath);
|
|
2160
|
+
continue;
|
|
2161
|
+
}
|
|
2162
|
+
files.push(entryPath);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
return files;
|
|
2166
|
+
}
|
|
2167
|
+
function directoryToRoutePattern(relativeDirectory) {
|
|
2168
|
+
const segments = relativeDirectory.split(path.sep).filter(Boolean).filter((segment) => !/^\(.+\)$/.test(segment) && !segment.startsWith("@")).map((segment) => segment.replace(/^\(\.{1,3}\)/, ""));
|
|
2169
|
+
return segments.length ? `/${segments.join("/")}` : "/";
|
|
2170
|
+
}
|
|
2171
|
+
function matchRoutePattern(pattern, pathname) {
|
|
2172
|
+
const patternSegments = splitPath(pattern);
|
|
2173
|
+
const pathSegments = splitPath(pathname);
|
|
2174
|
+
const params = {};
|
|
2175
|
+
let score = 0;
|
|
2176
|
+
let pathIndex = 0;
|
|
2177
|
+
for (const segment of patternSegments) {
|
|
2178
|
+
const optionalCatchAll = segment.match(/^\[\[\.\.\.(.+)\]\]$/);
|
|
2179
|
+
if (optionalCatchAll) {
|
|
2180
|
+
params[optionalCatchAll[1]] = pathSegments.slice(pathIndex);
|
|
2181
|
+
pathIndex = pathSegments.length;
|
|
2182
|
+
score += 1;
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
const catchAll = segment.match(/^\[\.\.\.(.+)\]$/);
|
|
2186
|
+
if (catchAll) {
|
|
2187
|
+
if (pathIndex >= pathSegments.length) return null;
|
|
2188
|
+
params[catchAll[1]] = pathSegments.slice(pathIndex);
|
|
2189
|
+
pathIndex = pathSegments.length;
|
|
2190
|
+
score += 10;
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
const dynamic = segment.match(/^\[(.+)\]$/);
|
|
2194
|
+
if (dynamic) {
|
|
2195
|
+
if (pathIndex >= pathSegments.length) return null;
|
|
2196
|
+
params[dynamic[1]] = pathSegments[pathIndex++];
|
|
2197
|
+
score += 50;
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
if (segment !== pathSegments[pathIndex++]) return null;
|
|
2201
|
+
score += 100;
|
|
2202
|
+
}
|
|
2203
|
+
return pathIndex === pathSegments.length ? {
|
|
2204
|
+
params,
|
|
2205
|
+
score
|
|
2206
|
+
} : null;
|
|
2207
|
+
}
|
|
2208
|
+
function collectInheritedRouteFiles(config, pathname, baseName, extensions) {
|
|
2209
|
+
return collectLayeredRouteFiles(config, baseName, extensions).filter((entry) => matchesRoutePrefix(entry.pattern, pathname)).sort(compareInheritedRouteFiles).map((entry) => entry.filePath);
|
|
2210
|
+
}
|
|
2211
|
+
function collectMiddleware(root, hasConfigMiddleware, config, pathname) {
|
|
2212
|
+
const rootMiddleware = /* @__PURE__ */ new Map();
|
|
2213
|
+
for (const source of getFarmSourceRoots(config)) {
|
|
2214
|
+
const filePath = findFile(path.join(source.root, source.srcDir), "middleware", MIDDLEWARE_EXTENSIONS);
|
|
2215
|
+
if (filePath) rootMiddleware.set("root", {
|
|
2216
|
+
filePath,
|
|
2217
|
+
pattern: "/"
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
const files = [...rootMiddleware.values(), ...collectLayeredRouteFiles(config, "middleware", MIDDLEWARE_EXTENSIONS).filter((entry) => matchesRoutePrefix(entry.pattern, pathname))].sort(compareInheritedRouteFiles).map((entry) => entry.filePath);
|
|
2221
|
+
return [...hasConfigMiddleware ? [{
|
|
2222
|
+
source: "config",
|
|
2223
|
+
filePath: "farm.config (middleware)"
|
|
2224
|
+
}] : [], ...files.map((filePath) => ({
|
|
2225
|
+
source: "file",
|
|
2226
|
+
filePath: toProjectPath(root, filePath)
|
|
2227
|
+
}))];
|
|
2228
|
+
}
|
|
2229
|
+
function collectLayeredRouteFiles(config, baseName, extensions) {
|
|
2230
|
+
const files = /* @__PURE__ */ new Map();
|
|
2231
|
+
const filePattern = new RegExp(`^${escapeRegExp(baseName)}\\.(?:${extensions.map(escapeRegExp).join("|")})$`);
|
|
2232
|
+
for (const source of getFarmSourceRoots(config)) {
|
|
2233
|
+
const appDirectory = path.join(source.root, source.srcDir, "app");
|
|
2234
|
+
if (!existsSync(appDirectory)) continue;
|
|
2235
|
+
for (const filePath of walkFiles(appDirectory)) {
|
|
2236
|
+
if (!filePattern.test(path.basename(filePath))) continue;
|
|
2237
|
+
const pattern = directoryToRoutePattern(path.relative(appDirectory, path.dirname(filePath)));
|
|
2238
|
+
files.set(pattern, {
|
|
2239
|
+
filePath,
|
|
2240
|
+
pattern
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return [...files.values()];
|
|
2245
|
+
}
|
|
2246
|
+
function findNearestSocialImage(config, pathname, baseName) {
|
|
2247
|
+
return collectLayeredRouteFiles(config, baseName, SOCIAL_IMAGE_EXTENSIONS).filter((entry) => matchesRoutePrefix(entry.pattern, pathname)).sort((left, right) => compareInheritedRouteFiles(right, left))[0]?.filePath;
|
|
2248
|
+
}
|
|
2249
|
+
function compareInheritedRouteFiles(left, right) {
|
|
2250
|
+
return splitPath(left.pattern).length - splitPath(right.pattern).length;
|
|
2251
|
+
}
|
|
2252
|
+
function matchesRoutePrefix(pattern, pathname) {
|
|
2253
|
+
const patternSegments = splitPath(pattern);
|
|
2254
|
+
const pathSegments = splitPath(pathname);
|
|
2255
|
+
if (patternSegments.length > pathSegments.length) return false;
|
|
2256
|
+
return patternSegments.every((segment, index) => {
|
|
2257
|
+
if (/^\[{1,2}(?:\.\.\.)?.+\]{1,2}$/.test(segment)) return true;
|
|
2258
|
+
return segment === pathSegments[index];
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
function findFile(directory, baseName, extensions) {
|
|
2262
|
+
return extensions.map((extension) => path.join(directory, `${baseName}.${extension}`)).find(existsSync);
|
|
2263
|
+
}
|
|
2264
|
+
function escapeRegExp(value) {
|
|
2265
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2266
|
+
}
|
|
2267
|
+
function readRuntimeExports(source) {
|
|
2268
|
+
const runtime = readStringExport(source, "runtime");
|
|
2269
|
+
const maxDuration = readNumberOrAutoExport(source, "maxDuration");
|
|
2270
|
+
const regions = readStringArrayOrAutoExport(source, "regions");
|
|
2271
|
+
return {
|
|
2272
|
+
...runtime === "auto" || runtime === "node" || runtime === "edge" ? { runtime } : {},
|
|
2273
|
+
...regions ? { regions } : {},
|
|
2274
|
+
...maxDuration !== void 0 ? { maxDuration } : {}
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
function resolveRendering(pageSource, matchingRules) {
|
|
2278
|
+
const pageRendering = resolveRouteRenderingConfig({
|
|
2279
|
+
...readBooleanExport(pageSource, "ssg") !== void 0 ? { ssg: readBooleanExport(pageSource, "ssg") } : {},
|
|
2280
|
+
...readBooleanExport(pageSource, "ppr") !== void 0 ? { ppr: readBooleanExport(pageSource, "ppr") } : {},
|
|
2281
|
+
...readBooleanExport(pageSource, "experimental_ppr") !== void 0 ? { experimental_ppr: readBooleanExport(pageSource, "experimental_ppr") } : {},
|
|
2282
|
+
...readNumberOrFalseExport(pageSource, "revalidate") !== void 0 ? { revalidate: readNumberOrFalseExport(pageSource, "revalidate") } : {},
|
|
2283
|
+
...readDynamicExport(pageSource) ? { dynamic: readDynamicExport(pageSource) } : {}
|
|
2284
|
+
}, pageSource);
|
|
2285
|
+
let mode = pageRendering.ssg ? "static" : pageRendering.ppr ? "partial" : "dynamic";
|
|
2286
|
+
let reason = pageRendering.directive ? `page directive ${JSON.stringify(pageRendering.directive)}` : pageRendering.ssg ? "page static rendering declaration" : pageRendering.ppr ? "page PPR declaration" : "default server rendering";
|
|
2287
|
+
let ppr = pageRendering.ppr;
|
|
2288
|
+
for (const [pattern, rule] of matchingRules) if (rule.prerender === true || rule.render === "static") {
|
|
2289
|
+
mode = "static";
|
|
2290
|
+
reason = `routeRules ${pattern}`;
|
|
2291
|
+
ppr = false;
|
|
2292
|
+
} else if (rule.prerender === false || rule.render === "dynamic" || rule.ssr === true) {
|
|
2293
|
+
mode = "dynamic";
|
|
2294
|
+
reason = `routeRules ${pattern}`;
|
|
2295
|
+
ppr = false;
|
|
2296
|
+
} else if (rule.ssr === false) {
|
|
2297
|
+
mode = "client";
|
|
2298
|
+
reason = `routeRules ${pattern}`;
|
|
2299
|
+
ppr = false;
|
|
2300
|
+
}
|
|
2301
|
+
return {
|
|
2302
|
+
mode,
|
|
2303
|
+
reason,
|
|
2304
|
+
ppr
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
function resolveCaching(pageSource, matchingRules) {
|
|
2308
|
+
let swr;
|
|
2309
|
+
let isr;
|
|
2310
|
+
for (const [, rule] of matchingRules) {
|
|
2311
|
+
if (typeof rule.swr === "number" || typeof rule.swr === "boolean") swr = rule.swr;
|
|
2312
|
+
if (typeof rule.isr === "number" || typeof rule.isr === "boolean") isr = rule.isr;
|
|
2313
|
+
}
|
|
2314
|
+
return {
|
|
2315
|
+
...readNumberOrFalseExport(pageSource, "revalidate") !== void 0 ? { revalidate: readNumberOrFalseExport(pageSource, "revalidate") } : {},
|
|
2316
|
+
...swr !== void 0 ? { swr } : {},
|
|
2317
|
+
...isr !== void 0 ? { isr } : {},
|
|
2318
|
+
rules: matchingRules.map(([pattern]) => pattern)
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
function readStringExport(source, name) {
|
|
2322
|
+
return source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*["']([^"']+)["']`))?.[1];
|
|
2323
|
+
}
|
|
2324
|
+
function readDynamicExport(source) {
|
|
2325
|
+
const dynamic = readStringExport(source, "dynamic");
|
|
2326
|
+
return dynamic === "auto" || dynamic === "force-dynamic" || dynamic === "error" || dynamic === "force-static" ? dynamic : void 0;
|
|
2327
|
+
}
|
|
2328
|
+
function readBooleanExport(source, name) {
|
|
2329
|
+
const value = source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*(true|false)`))?.[1];
|
|
2330
|
+
return value === void 0 ? void 0 : value === "true";
|
|
2331
|
+
}
|
|
2332
|
+
function readNumberOrAutoExport(source, name) {
|
|
2333
|
+
const value = source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*(?:["'](auto)["']|(\\d+))`));
|
|
2334
|
+
return value?.[1] === "auto" ? "auto" : value?.[2] ? Number(value[2]) : void 0;
|
|
2335
|
+
}
|
|
2336
|
+
function readNumberOrFalseExport(source, name) {
|
|
2337
|
+
const value = source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*(false|\\d+)`))?.[1];
|
|
2338
|
+
return value === "false" ? false : value ? Number(value) : void 0;
|
|
2339
|
+
}
|
|
2340
|
+
function readStringArrayOrAutoExport(source, name) {
|
|
2341
|
+
if (source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*["']auto["']`))) return "auto";
|
|
2342
|
+
const array = source.match(new RegExp(`export\\s+const\\s+${name}\\s*=\\s*\\[([^\\]]*)\\]`))?.[1];
|
|
2343
|
+
if (array === void 0) return void 0;
|
|
2344
|
+
return [...array.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
|
|
2345
|
+
}
|
|
2346
|
+
function routeSpecificity(pattern) {
|
|
2347
|
+
return splitPath(pattern).reduce((score, segment) => {
|
|
2348
|
+
if (segment === "**" || segment.startsWith("[[...")) return score + 1;
|
|
2349
|
+
if (segment === "*" || segment.startsWith("[...")) return score + 10;
|
|
2350
|
+
if (segment.startsWith("[") || segment.startsWith(":")) return score + 50;
|
|
2351
|
+
return score + 100;
|
|
2352
|
+
}, 0);
|
|
2353
|
+
}
|
|
2354
|
+
function normalizePathname(value, basePath) {
|
|
2355
|
+
let pathname;
|
|
2356
|
+
try {
|
|
2357
|
+
pathname = new URL(value, "http://farm.local").pathname;
|
|
2358
|
+
} catch {
|
|
2359
|
+
pathname = value;
|
|
2360
|
+
}
|
|
2361
|
+
pathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
2362
|
+
const normalizedBase = basePath && basePath !== "/" ? `/${basePath.replace(/^\/+|\/+$/g, "")}` : "";
|
|
2363
|
+
if (normalizedBase && (pathname === normalizedBase || pathname.startsWith(`${normalizedBase}/`))) pathname = pathname.slice(normalizedBase.length) || "/";
|
|
2364
|
+
return pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
|
|
2365
|
+
}
|
|
2366
|
+
function splitPath(value) {
|
|
2367
|
+
return value.split("/").filter(Boolean).map(decodeURIComponent);
|
|
2368
|
+
}
|
|
2369
|
+
function toProjectPath(root, filePath) {
|
|
2370
|
+
let relativePath = path.relative(root, filePath);
|
|
2371
|
+
if ((relativePath === ".." || relativePath.startsWith(`..${path.sep}`)) && existsSync(root) && existsSync(filePath)) relativePath = path.relative(realpathSync(root), realpathSync(filePath));
|
|
2372
|
+
return relativePath.split(path.sep).join("/");
|
|
2373
|
+
}
|
|
2374
|
+
function formatParams(params) {
|
|
2375
|
+
const entries = Object.entries(params);
|
|
2376
|
+
return entries.length ? entries.map(([key, value]) => `${key}=${Array.isArray(value) ? value.join("/") : value}`).join(", ") : "none";
|
|
2377
|
+
}
|
|
2378
|
+
function formatRuntime(runtime) {
|
|
2379
|
+
return [
|
|
2380
|
+
runtime.runtime,
|
|
2381
|
+
runtime.regions?.length ? `regions=${runtime.regions.join(",")}` : "",
|
|
2382
|
+
runtime.maxDuration ? `maxDuration=${runtime.maxDuration}s` : ""
|
|
2383
|
+
].filter(Boolean).join(", ");
|
|
2384
|
+
}
|
|
2385
|
+
function formatCaching(cache) {
|
|
2386
|
+
const values = [
|
|
2387
|
+
cache.revalidate !== void 0 ? `revalidate=${cache.revalidate}` : "",
|
|
2388
|
+
cache.swr !== void 0 ? `swr=${cache.swr}` : "",
|
|
2389
|
+
cache.isr !== void 0 ? `isr=${cache.isr}` : "",
|
|
2390
|
+
cache.rules.length ? `rules=${cache.rules.join(",")}` : ""
|
|
2391
|
+
].filter(Boolean);
|
|
2392
|
+
return values.length ? values.join("; ") : "request-time / no declared cache";
|
|
2393
|
+
}
|
|
2394
|
+
function formatMetadata(metadata) {
|
|
2395
|
+
const values = [
|
|
2396
|
+
metadata.static.length ? `static=${metadata.static.join(",")}` : "",
|
|
2397
|
+
metadata.dynamic.length ? `dynamic=${metadata.dynamic.join(",")}` : "",
|
|
2398
|
+
metadata.openGraphImage ? `og=${metadata.openGraphImage}` : "",
|
|
2399
|
+
metadata.twitterImage ? `twitter=${metadata.twitterImage}` : ""
|
|
2400
|
+
].filter(Boolean);
|
|
2401
|
+
return values.length ? values.join("; ") : "none";
|
|
2402
|
+
}
|
|
2403
|
+
//#endregion
|
|
1802
2404
|
//#region src/cron.ts
|
|
1803
2405
|
async function loadFarmCronConfig(options = {}) {
|
|
1804
2406
|
const root = path.resolve(options.root || process.cwd());
|
|
@@ -2603,6 +3205,6 @@ function formatError(error) {
|
|
|
2603
3205
|
return error instanceof Error ? error.message : String(error);
|
|
2604
3206
|
}
|
|
2605
3207
|
//#endregion
|
|
2606
|
-
export { addFarmIntegration, buildFarm, createFarmUpgradePlan, createFrameworkMigrationPlan, createGatewaySession, createPreviewGatewayPlan, createPreviewTunnelPlan, createServer, deployFarm, detectFarmPackageManager, formatFarmCronJobs, formatFarmDoctorReport, formatFarmUpgradePlan, forwardGatewayRequest, generateFarmArtifacts, inspectFrameworkMigrations, listFarmCronJobs, listFarmIntegrationProviders, loadFarmCronConfig, migrateFarm, migrateFarmAuth, parsePreviewPublicUrl, previewFarm, resolveCloudflareAgentDeployPlan, resolvePreviewTarget, runFarmCronJob, runFarmDoctor, runPreviewGateway, startDevServer, startFarmCronScheduler, upgradeFarm };
|
|
3208
|
+
export { FarmDeployError, FarmGeneratedArtifactsStaleError, addFarmIntegration, buildFarm, createFarmDeployPlan, createFarmUpgradePlan, createFrameworkMigrationPlan, createGatewaySession, createPreviewGatewayPlan, createPreviewTunnelPlan, createServer, deployFarm, detectFarmPackageManager, explainFarmRoute, formatFarmCronJobs, formatFarmDeployPlan, formatFarmDoctorReport, formatFarmRouteExplanation, formatFarmUpgradePlan, forwardGatewayRequest, generateFarmArtifacts, inspectFrameworkMigrations, listFarmCronJobs, listFarmIntegrationProviders, loadFarmCronConfig, migrateFarm, migrateFarmAuth, parsePreviewPublicUrl, previewFarm, resolveCloudflareAgentDeployPlan, resolvePreviewTarget, runFarmCronJob, runFarmDoctor, runPreviewGateway, startDevServer, startFarmCronScheduler, upgradeFarm };
|
|
2607
3209
|
|
|
2608
3210
|
//# sourceMappingURL=index.mjs.map
|