@vercel/python 6.40.0 → 6.41.0
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.js +259 -236
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -5215,166 +5215,17 @@ async function generateProjectManifest({
|
|
|
5215
5215
|
var diagnostics = (0, import_build_utils6.createDiagnostics)("python");
|
|
5216
5216
|
|
|
5217
5217
|
// src/crons.ts
|
|
5218
|
-
var
|
|
5219
|
-
var
|
|
5218
|
+
var import_fs8 = __toESM(require("fs"));
|
|
5219
|
+
var import_path8 = require("path");
|
|
5220
5220
|
var import_execa4 = __toESM(require_execa());
|
|
5221
|
-
var
|
|
5222
|
-
var DYNAMIC_SCHEDULE = "<dynamic>";
|
|
5223
|
-
function entrypointToModule(entrypoint) {
|
|
5224
|
-
return entrypoint.replace(/\\/g, "/").replace(/\.py$/i, "").replace(/\//g, ".");
|
|
5225
|
-
}
|
|
5226
|
-
var scriptPath = (0, import_path7.join)(__dirname, "..", "templates", "vc_cron_detect.py");
|
|
5227
|
-
var script = import_fs7.default.readFileSync(scriptPath, "utf-8");
|
|
5228
|
-
function buildCronRouteTable(crons) {
|
|
5229
|
-
const table = {};
|
|
5230
|
-
for (const cron of crons) {
|
|
5231
|
-
table[cron.path] = cron.resolvedHandler;
|
|
5232
|
-
}
|
|
5233
|
-
return table;
|
|
5234
|
-
}
|
|
5235
|
-
async function getServiceCrons(opts) {
|
|
5236
|
-
const { service, entrypoint, rawEntrypoint, handlerFunction } = opts;
|
|
5237
|
-
const isScheduledService = !!service && (0, import_build_utils7.isScheduleTriggeredService)(service);
|
|
5238
|
-
if (!isScheduledService || !service.name || typeof service.schedule !== "string" && !Array.isArray(service.schedule)) {
|
|
5239
|
-
return void 0;
|
|
5240
|
-
}
|
|
5241
|
-
const cronEntrypoint = entrypoint || rawEntrypoint;
|
|
5242
|
-
if (!cronEntrypoint) {
|
|
5243
|
-
throw new import_build_utils7.NowBuildError({
|
|
5244
|
-
code: "PYTHON_CRON_NO_ENTRYPOINT",
|
|
5245
|
-
message: "Cron service is missing an entrypoint."
|
|
5246
|
-
});
|
|
5247
|
-
}
|
|
5248
|
-
if (service.schedule === DYNAMIC_SCHEDULE) {
|
|
5249
|
-
return getServiceCronsDynamic({
|
|
5250
|
-
serviceName: service.name,
|
|
5251
|
-
cronEntrypoint,
|
|
5252
|
-
handlerFunction,
|
|
5253
|
-
pythonBin: opts.pythonBin,
|
|
5254
|
-
env: opts.env,
|
|
5255
|
-
workPath: opts.workPath
|
|
5256
|
-
});
|
|
5257
|
-
}
|
|
5258
|
-
const cronPath = (0, import_build_utils7.getInternalServiceCronPath)(
|
|
5259
|
-
service.name,
|
|
5260
|
-
cronEntrypoint,
|
|
5261
|
-
handlerFunction || "cron"
|
|
5262
|
-
);
|
|
5263
|
-
const moduleName = entrypointToModule(cronEntrypoint);
|
|
5264
|
-
const resolvedHandler = handlerFunction ? `${moduleName}:${handlerFunction}` : moduleName;
|
|
5265
|
-
const schedules = Array.isArray(service.schedule) ? service.schedule : [service.schedule];
|
|
5266
|
-
return schedules.map((schedule) => ({
|
|
5267
|
-
path: cronPath,
|
|
5268
|
-
schedule,
|
|
5269
|
-
resolvedHandler
|
|
5270
|
-
}));
|
|
5271
|
-
}
|
|
5272
|
-
async function getServiceCronsDynamic(opts) {
|
|
5273
|
-
const {
|
|
5274
|
-
serviceName,
|
|
5275
|
-
cronEntrypoint,
|
|
5276
|
-
handlerFunction,
|
|
5277
|
-
pythonBin,
|
|
5278
|
-
env,
|
|
5279
|
-
workPath
|
|
5280
|
-
} = opts;
|
|
5281
|
-
if (!handlerFunction) {
|
|
5282
|
-
throw new import_build_utils7.NowBuildError({
|
|
5283
|
-
code: "PYTHON_DYNAMIC_CRON_NO_HANDLER",
|
|
5284
|
-
message: 'Dynamic cron detection requires a "module:object" entrypoint where the object has a get_crons() method.'
|
|
5285
|
-
});
|
|
5286
|
-
}
|
|
5287
|
-
const moduleName = entrypointToModule(cronEntrypoint);
|
|
5288
|
-
const entries = await detectDynamicCrons({
|
|
5289
|
-
pythonBin,
|
|
5290
|
-
env,
|
|
5291
|
-
workPath,
|
|
5292
|
-
moduleName,
|
|
5293
|
-
attrName: handlerFunction
|
|
5294
|
-
});
|
|
5295
|
-
console.log(
|
|
5296
|
-
`Detected ${entries.length} cron entry(s): ${entries.map((e) => `${e.module_function} (${e.schedule})`).join(", ")}`
|
|
5297
|
-
);
|
|
5298
|
-
if (entries.length === 0) {
|
|
5299
|
-
throw new import_build_utils7.NowBuildError({
|
|
5300
|
-
code: "PYTHON_DYNAMIC_CRON_EMPTY",
|
|
5301
|
-
message: `Dynamic cron detection returned no entries. "${moduleName}.${handlerFunction}.get_crons()" returned an empty iterable.`
|
|
5302
|
-
});
|
|
5303
|
-
}
|
|
5304
|
-
return entries.map((entry) => {
|
|
5305
|
-
const cronPath = moduleColonFuncToCronPath(
|
|
5306
|
-
serviceName,
|
|
5307
|
-
entry.module_function
|
|
5308
|
-
);
|
|
5309
|
-
return {
|
|
5310
|
-
path: cronPath,
|
|
5311
|
-
schedule: entry.schedule,
|
|
5312
|
-
resolvedHandler: entry.module_function
|
|
5313
|
-
};
|
|
5314
|
-
});
|
|
5315
|
-
}
|
|
5316
|
-
async function detectDynamicCrons(opts) {
|
|
5317
|
-
const { pythonBin, env, workPath, moduleName, attrName } = opts;
|
|
5318
|
-
let stdout;
|
|
5319
|
-
try {
|
|
5320
|
-
const result = await (0, import_execa4.default)(
|
|
5321
|
-
pythonBin,
|
|
5322
|
-
["-c", script, moduleName, attrName],
|
|
5323
|
-
{ env, cwd: workPath }
|
|
5324
|
-
);
|
|
5325
|
-
stdout = result.stdout;
|
|
5326
|
-
} catch (err) {
|
|
5327
|
-
let detail = err?.stderr || err?.message || String(err);
|
|
5328
|
-
try {
|
|
5329
|
-
const parsed2 = JSON.parse(err?.stdout);
|
|
5330
|
-
if (parsed2.error)
|
|
5331
|
-
detail = parsed2.error;
|
|
5332
|
-
} catch {
|
|
5333
|
-
}
|
|
5334
|
-
throw new import_build_utils7.NowBuildError({
|
|
5335
|
-
code: "PYTHON_DYNAMIC_CRON_DETECTION_FAILED",
|
|
5336
|
-
message: `Failed to detect dynamic cron entries: ${detail}`
|
|
5337
|
-
});
|
|
5338
|
-
}
|
|
5339
|
-
let parsed;
|
|
5340
|
-
try {
|
|
5341
|
-
parsed = JSON.parse(stdout);
|
|
5342
|
-
} catch {
|
|
5343
|
-
throw new import_build_utils7.NowBuildError({
|
|
5344
|
-
code: "PYTHON_DYNAMIC_CRON_DETECTION_FAILED",
|
|
5345
|
-
message: `Dynamic cron detection returned invalid JSON: ${stdout}`
|
|
5346
|
-
});
|
|
5347
|
-
}
|
|
5348
|
-
return parsed.entries || [];
|
|
5349
|
-
}
|
|
5350
|
-
function moduleColonFuncToCronPath(serviceName, moduleFunction) {
|
|
5351
|
-
const colonIdx = moduleFunction.indexOf(":");
|
|
5352
|
-
if (colonIdx === -1) {
|
|
5353
|
-
throw new import_build_utils7.NowBuildError({
|
|
5354
|
-
code: "PYTHON_DYNAMIC_CRON_INVALID_FORMAT",
|
|
5355
|
-
message: `Dynamic cron entry must use "module:function" format, got: "${moduleFunction}"`
|
|
5356
|
-
});
|
|
5357
|
-
}
|
|
5358
|
-
const modulePart = moduleFunction.slice(0, colonIdx);
|
|
5359
|
-
const funcPart = moduleFunction.slice(colonIdx + 1);
|
|
5360
|
-
const entrypointPath = modulePart.replace(/\./g, "/");
|
|
5361
|
-
return (0, import_build_utils7.getInternalServiceCronPath)(serviceName, entrypointPath, funcPart);
|
|
5362
|
-
}
|
|
5363
|
-
|
|
5364
|
-
// src/start-dev-server.ts
|
|
5365
|
-
var import_child_process2 = require("child_process");
|
|
5366
|
-
var import_fs9 = require("fs");
|
|
5367
|
-
var import_path9 = require("path");
|
|
5368
|
-
var import_build_utils11 = require("@vercel/build-utils");
|
|
5369
|
-
var import_get_port = __toESM(require_get_port());
|
|
5370
|
-
var import_is_port_reachable = __toESM(require_is_port_reachable());
|
|
5221
|
+
var import_build_utils10 = require("@vercel/build-utils");
|
|
5371
5222
|
|
|
5372
5223
|
// src/entrypoint.ts
|
|
5373
|
-
var
|
|
5374
|
-
var
|
|
5224
|
+
var import_fs7 = __toESM(require("fs"));
|
|
5225
|
+
var import_path7 = require("path");
|
|
5226
|
+
var import_build_utils7 = require("@vercel/build-utils");
|
|
5375
5227
|
var import_build_utils8 = require("@vercel/build-utils");
|
|
5376
5228
|
var import_build_utils9 = require("@vercel/build-utils");
|
|
5377
|
-
var import_build_utils10 = require("@vercel/build-utils");
|
|
5378
5229
|
var import_python_analysis5 = require("@vercel/python-analysis");
|
|
5379
5230
|
var PYTHON_ENTRYPOINT_FILENAMES = [
|
|
5380
5231
|
"app",
|
|
@@ -5389,16 +5240,17 @@ var PYTHON_CANDIDATE_ENTRYPOINTS = getCandidateEntrypointsInDirs(
|
|
|
5389
5240
|
PYTHON_ENTRYPOINT_DIRS
|
|
5390
5241
|
);
|
|
5391
5242
|
var PYTHON_ENTRYPOINT_DOCS_URL = "https://vercel.com/docs/functions/runtimes/python#python-entrypoints";
|
|
5243
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["__pycache__", "node_modules"]);
|
|
5392
5244
|
function getCandidateEntrypointsInDirs(dirs) {
|
|
5393
5245
|
return dirs.flatMap(
|
|
5394
5246
|
(dir) => PYTHON_ENTRYPOINT_FILENAMES.map(
|
|
5395
|
-
(filename) =>
|
|
5247
|
+
(filename) => import_path7.posix.join(dir, `${filename}.py`)
|
|
5396
5248
|
)
|
|
5397
5249
|
);
|
|
5398
5250
|
}
|
|
5399
5251
|
async function fileExists(filePath) {
|
|
5400
5252
|
try {
|
|
5401
|
-
const stat = await
|
|
5253
|
+
const stat = await import_fs7.default.promises.stat(filePath);
|
|
5402
5254
|
return stat.isFile();
|
|
5403
5255
|
} catch {
|
|
5404
5256
|
return false;
|
|
@@ -5449,34 +5301,67 @@ function getMissingExportMessage(framework, entrypoints) {
|
|
|
5449
5301
|
return entrypoints.length === 1 ? `Found ${fileList} but it does not export a top-level "app", "application", or "handler" variable.` : `Found ${fileList} but none export a top-level "app", "application", or "handler" variable.`;
|
|
5450
5302
|
}
|
|
5451
5303
|
}
|
|
5304
|
+
function entrypointToModule(entrypoint) {
|
|
5305
|
+
return entrypoint.replace(/\\/g, "/").replace(/\.py$/i, "").replace(/\//g, ".");
|
|
5306
|
+
}
|
|
5452
5307
|
async function checkEntrypoint(workPath, relPath) {
|
|
5453
|
-
const absPath = (0,
|
|
5308
|
+
const absPath = (0, import_path7.join)(workPath, relPath);
|
|
5454
5309
|
if (!await fileExists(absPath))
|
|
5455
5310
|
return null;
|
|
5456
|
-
const content = await
|
|
5311
|
+
const content = await import_fs7.default.promises.readFile(absPath, "utf-8");
|
|
5457
5312
|
return (0, import_python_analysis5.findAppOrHandler)(content);
|
|
5458
5313
|
}
|
|
5459
|
-
async function
|
|
5314
|
+
async function findPythonFilesRecursive(dir, rootDir) {
|
|
5460
5315
|
const results = [];
|
|
5461
|
-
|
|
5462
|
-
|
|
5316
|
+
let entries;
|
|
5317
|
+
try {
|
|
5318
|
+
entries = await import_fs7.default.promises.readdir(dir, { withFileTypes: true });
|
|
5319
|
+
} catch {
|
|
5320
|
+
return results;
|
|
5321
|
+
}
|
|
5322
|
+
for (const entry of entries) {
|
|
5323
|
+
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name))
|
|
5324
|
+
continue;
|
|
5325
|
+
const fullPath = (0, import_path7.join)(dir, entry.name);
|
|
5326
|
+
if (entry.isDirectory()) {
|
|
5327
|
+
results.push(...await findPythonFilesRecursive(fullPath, rootDir));
|
|
5328
|
+
} else if (entry.isFile() && entry.name.endsWith(".py")) {
|
|
5329
|
+
results.push((0, import_path7.relative)(rootDir, fullPath).replace(/\\/g, "/"));
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
5332
|
+
return results;
|
|
5333
|
+
}
|
|
5334
|
+
async function findEntrypointCandidates(workPath) {
|
|
5335
|
+
const pyFiles = await findPythonFilesRecursive(workPath, workPath);
|
|
5336
|
+
const candidates = [];
|
|
5337
|
+
for (const relPath of pyFiles) {
|
|
5463
5338
|
try {
|
|
5464
|
-
const
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
const stat = await import_fs8.default.promises.stat((0, import_path8.join)(dirPath, entry));
|
|
5472
|
-
if (stat.isFile()) {
|
|
5473
|
-
results.push(relPath);
|
|
5474
|
-
}
|
|
5339
|
+
const content = await import_fs7.default.promises.readFile(
|
|
5340
|
+
(0, import_path7.join)(workPath, relPath),
|
|
5341
|
+
"utf-8"
|
|
5342
|
+
);
|
|
5343
|
+
const varName = await (0, import_python_analysis5.findAppOrHandler)(content);
|
|
5344
|
+
if (varName) {
|
|
5345
|
+
candidates.push({ entrypoint: relPath, variableName: varName });
|
|
5475
5346
|
}
|
|
5476
5347
|
} catch {
|
|
5477
5348
|
}
|
|
5478
5349
|
}
|
|
5479
|
-
return
|
|
5350
|
+
return candidates;
|
|
5351
|
+
}
|
|
5352
|
+
function renderCandidateSuggestion(candidates) {
|
|
5353
|
+
const lines = candidates.map(
|
|
5354
|
+
(c) => ` ${c.entrypoint} (variable: ${c.variableName})`
|
|
5355
|
+
);
|
|
5356
|
+
const suggested = candidates[0];
|
|
5357
|
+
const moduleAttr = `${entrypointToModule(suggested.entrypoint)}:${suggested.variableName}`;
|
|
5358
|
+
return `but found potential entrypoints:
|
|
5359
|
+
${lines.join("\n")}
|
|
5360
|
+
|
|
5361
|
+
Add this to your pyproject.toml:
|
|
5362
|
+
|
|
5363
|
+
[tool.vercel]
|
|
5364
|
+
entrypoint = "${moduleAttr}"`;
|
|
5480
5365
|
}
|
|
5481
5366
|
async function resolveModuleAttrEntrypoint(workPath, value) {
|
|
5482
5367
|
const match = value.match(/([A-Za-z_][\w.]*)\s*:\s*([A-Za-z_][\w]*)/);
|
|
@@ -5487,14 +5372,14 @@ async function resolveModuleAttrEntrypoint(workPath, value) {
|
|
|
5487
5372
|
const relPath = modulePath.replace(/\./g, "/");
|
|
5488
5373
|
const candidates = [`${relPath}.py`, `${relPath}/__init__.py`];
|
|
5489
5374
|
for (const candidate of candidates) {
|
|
5490
|
-
if (await fileExists((0,
|
|
5375
|
+
if (await fileExists((0, import_path7.join)(workPath, candidate))) {
|
|
5491
5376
|
return { entrypoint: candidate, variableName };
|
|
5492
5377
|
}
|
|
5493
5378
|
}
|
|
5494
5379
|
return null;
|
|
5495
5380
|
}
|
|
5496
5381
|
async function getVercelToolsEntrypoint(workPath) {
|
|
5497
|
-
const pyprojectData = await (0,
|
|
5382
|
+
const pyprojectData = await (0, import_build_utils9.readConfigFile)((0, import_path7.join)(workPath, "pyproject.toml"));
|
|
5498
5383
|
if (!pyprojectData)
|
|
5499
5384
|
return null;
|
|
5500
5385
|
const vercelEntrypoint = pyprojectData.tool?.vercel?.entrypoint;
|
|
@@ -5503,7 +5388,7 @@ async function getVercelToolsEntrypoint(workPath) {
|
|
|
5503
5388
|
return resolveModuleAttrEntrypoint(workPath, vercelEntrypoint);
|
|
5504
5389
|
}
|
|
5505
5390
|
async function getPyprojectEntrypointWithDiagnostics(workPath) {
|
|
5506
|
-
const pyprojectData = await (0,
|
|
5391
|
+
const pyprojectData = await (0, import_build_utils9.readConfigFile)((0, import_path7.join)(workPath, "pyproject.toml"));
|
|
5507
5392
|
if (!pyprojectData)
|
|
5508
5393
|
return { entrypoint: null };
|
|
5509
5394
|
const scripts = pyprojectData.project?.scripts;
|
|
@@ -5518,7 +5403,7 @@ async function getPyprojectEntrypointWithDiagnostics(workPath) {
|
|
|
5518
5403
|
const relPath = modulePath.replace(/\./g, "/");
|
|
5519
5404
|
const candidates = [`${relPath}.py`, `${relPath}/__init__.py`];
|
|
5520
5405
|
for (const candidate of candidates) {
|
|
5521
|
-
if (await fileExists((0,
|
|
5406
|
+
if (await fileExists((0, import_path7.join)(workPath, candidate))) {
|
|
5522
5407
|
return { entrypoint: { entrypoint: candidate, variableName } };
|
|
5523
5408
|
}
|
|
5524
5409
|
}
|
|
@@ -5527,13 +5412,13 @@ async function getPyprojectEntrypointWithDiagnostics(workPath) {
|
|
|
5527
5412
|
async function findValidEntrypoint(workPath, candidates) {
|
|
5528
5413
|
const existingWithoutEntrypoint = [];
|
|
5529
5414
|
for (const candidate of candidates) {
|
|
5530
|
-
const absPath = (0,
|
|
5415
|
+
const absPath = (0, import_path7.join)(workPath, candidate);
|
|
5531
5416
|
if (!await fileExists(absPath))
|
|
5532
5417
|
continue;
|
|
5533
|
-
const content = await
|
|
5418
|
+
const content = await import_fs7.default.promises.readFile(absPath, "utf-8");
|
|
5534
5419
|
const varName = await (0, import_python_analysis5.findAppOrHandler)(content);
|
|
5535
5420
|
if (varName) {
|
|
5536
|
-
(0,
|
|
5421
|
+
(0, import_build_utils8.debug)(`Detected Python entrypoint: ${candidate} (variable: ${varName})`);
|
|
5537
5422
|
return {
|
|
5538
5423
|
entrypoint: { entrypoint: candidate, variableName: varName },
|
|
5539
5424
|
existingWithoutEntrypoint
|
|
@@ -5544,12 +5429,12 @@ async function findValidEntrypoint(workPath, candidates) {
|
|
|
5544
5429
|
return { entrypoint: null, existingWithoutEntrypoint };
|
|
5545
5430
|
}
|
|
5546
5431
|
async function checkDjangoManage(workPath) {
|
|
5547
|
-
const managePath = (0,
|
|
5432
|
+
const managePath = (0, import_path7.join)(workPath, "manage.py");
|
|
5548
5433
|
try {
|
|
5549
|
-
const content = await
|
|
5434
|
+
const content = await import_fs7.default.promises.readFile(managePath, "utf-8");
|
|
5550
5435
|
if (!content.includes("DJANGO_SETTINGS_MODULE"))
|
|
5551
5436
|
return false;
|
|
5552
|
-
(0,
|
|
5437
|
+
(0, import_build_utils8.debug)(`Found Django manage.py with DJANGO_SETTINGS_MODULE at ${workPath}`);
|
|
5553
5438
|
return true;
|
|
5554
5439
|
} catch {
|
|
5555
5440
|
return false;
|
|
@@ -5557,7 +5442,7 @@ async function checkDjangoManage(workPath) {
|
|
|
5557
5442
|
}
|
|
5558
5443
|
async function getSubdirectories(workPath) {
|
|
5559
5444
|
try {
|
|
5560
|
-
const entries = await
|
|
5445
|
+
const entries = await import_fs7.default.promises.readdir(workPath, {
|
|
5561
5446
|
withFileTypes: true
|
|
5562
5447
|
});
|
|
5563
5448
|
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
@@ -5570,23 +5455,39 @@ function makeDiagnosticError(framework, diagnostics2) {
|
|
|
5570
5455
|
const link = getFrameworkEntrypointDocsUrl(framework);
|
|
5571
5456
|
const action = "Learn More";
|
|
5572
5457
|
const displayName = getFrameworkDisplayName(framework);
|
|
5458
|
+
const elsewhere = diagnostics2.validCandidatesElsewhere ?? [];
|
|
5459
|
+
const suggestion = elsewhere.length > 0 ? renderCandidateSuggestion(elsewhere) : null;
|
|
5573
5460
|
let message;
|
|
5574
5461
|
if (diagnostics2.existingWithoutEntrypoint.length > 0) {
|
|
5575
5462
|
message = getMissingExportMessage(
|
|
5576
5463
|
framework,
|
|
5577
5464
|
diagnostics2.existingWithoutEntrypoint
|
|
5578
5465
|
);
|
|
5466
|
+
if (suggestion)
|
|
5467
|
+
message += `
|
|
5468
|
+
|
|
5469
|
+
${suggestion}`;
|
|
5470
|
+
} else if (diagnostics2.djangoManageBaseDir !== void 0) {
|
|
5471
|
+
const where = diagnostics2.djangoManageBaseDir || ".";
|
|
5472
|
+
message = `Found Django manage.py in "${where}" but could not resolve the application entrypoint. Ensure WSGI_APPLICATION or ASGI_APPLICATION is set in your Django settings.`;
|
|
5473
|
+
if (suggestion)
|
|
5474
|
+
message += `
|
|
5475
|
+
|
|
5476
|
+
${suggestion}`;
|
|
5579
5477
|
} else if (diagnostics2.pyprojectAppScript && diagnostics2.pyprojectCheckedPaths?.length) {
|
|
5580
5478
|
const checked = diagnostics2.pyprojectCheckedPaths.join(" or ");
|
|
5581
5479
|
message = `pyproject.toml [project.scripts] defines app = "${diagnostics2.pyprojectAppScript}" but ${checked} was not found.`;
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5480
|
+
if (suggestion)
|
|
5481
|
+
message += `
|
|
5482
|
+
|
|
5483
|
+
${suggestion}`;
|
|
5484
|
+
} else if (suggestion) {
|
|
5485
|
+
message = `No ${displayName} entrypoint found in default locations, ${suggestion}`;
|
|
5585
5486
|
} else {
|
|
5586
5487
|
const searchedList = PYTHON_CANDIDATE_ENTRYPOINTS.join(", ");
|
|
5587
5488
|
message = `No ${displayName} entrypoint found. Set "tool.vercel.entrypoint" in pyproject.toml or define an entrypoint in one of: ${searchedList}.`;
|
|
5588
5489
|
}
|
|
5589
|
-
return new
|
|
5490
|
+
return new import_build_utils7.NowBuildError({ code, message, link, action });
|
|
5590
5491
|
}
|
|
5591
5492
|
async function detectGenericPythonEntrypoint(workPath) {
|
|
5592
5493
|
try {
|
|
@@ -5599,7 +5500,7 @@ async function detectGenericPythonEntrypoint(workPath) {
|
|
|
5599
5500
|
findDiagnostics: result
|
|
5600
5501
|
};
|
|
5601
5502
|
} catch {
|
|
5602
|
-
(0,
|
|
5503
|
+
(0, import_build_utils8.debug)("Failed to discover Python entrypoint");
|
|
5603
5504
|
return {
|
|
5604
5505
|
detected: null,
|
|
5605
5506
|
findDiagnostics: { entrypoint: null, existingWithoutEntrypoint: [] }
|
|
@@ -5612,20 +5513,18 @@ async function detectDjangoPythonEntrypoint(workPath) {
|
|
|
5612
5513
|
findDiagnostics: {
|
|
5613
5514
|
entrypoint: null,
|
|
5614
5515
|
existingWithoutEntrypoint: []
|
|
5615
|
-
}
|
|
5616
|
-
dirs: PYTHON_ENTRYPOINT_DIRS
|
|
5516
|
+
}
|
|
5617
5517
|
};
|
|
5618
5518
|
try {
|
|
5619
5519
|
const subdirs = await getSubdirectories(workPath);
|
|
5620
5520
|
const rootDirs = ["", ...subdirs];
|
|
5621
5521
|
for (const rootDir of rootDirs) {
|
|
5622
|
-
const currPath = (0,
|
|
5522
|
+
const currPath = (0, import_path7.join)(workPath, rootDir);
|
|
5623
5523
|
const isDjango = await checkDjangoManage(currPath);
|
|
5624
5524
|
if (isDjango) {
|
|
5625
5525
|
return {
|
|
5626
5526
|
detected: { baseDir: rootDir },
|
|
5627
|
-
findDiagnostics: { entrypoint: null, existingWithoutEntrypoint: [] }
|
|
5628
|
-
dirs: rootDirs
|
|
5527
|
+
findDiagnostics: { entrypoint: null, existingWithoutEntrypoint: [] }
|
|
5629
5528
|
};
|
|
5630
5529
|
}
|
|
5631
5530
|
}
|
|
@@ -5633,11 +5532,10 @@ async function detectDjangoPythonEntrypoint(workPath) {
|
|
|
5633
5532
|
const result = await findValidEntrypoint(workPath, candidates);
|
|
5634
5533
|
return {
|
|
5635
5534
|
detected: result.entrypoint ? { entrypoint: result.entrypoint } : null,
|
|
5636
|
-
findDiagnostics: result
|
|
5637
|
-
dirs: rootDirs
|
|
5535
|
+
findDiagnostics: result
|
|
5638
5536
|
};
|
|
5639
5537
|
} catch {
|
|
5640
|
-
(0,
|
|
5538
|
+
(0, import_build_utils8.debug)("Failed to discover Django Python entrypoint");
|
|
5641
5539
|
return emptyResult;
|
|
5642
5540
|
}
|
|
5643
5541
|
}
|
|
@@ -5645,10 +5543,10 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
|
|
|
5645
5543
|
if (configuredEntrypoint) {
|
|
5646
5544
|
const { filePath: configEntryFile, varName: configEntryVar } = configuredEntrypoint;
|
|
5647
5545
|
const entrypoint = configEntryFile.endsWith(".py") ? configEntryFile : `${configEntryFile}.py`;
|
|
5648
|
-
const entrypointExists = await fileExists((0,
|
|
5546
|
+
const entrypointExists = await fileExists((0, import_path7.join)(workPath, entrypoint));
|
|
5649
5547
|
if (!entrypointExists) {
|
|
5650
5548
|
return {
|
|
5651
|
-
error: new
|
|
5549
|
+
error: new import_build_utils7.NowBuildError({
|
|
5652
5550
|
code: "PYTHON_ENTRYPOINT_NOT_FOUND",
|
|
5653
5551
|
message: `Configured Python entrypoint "${entrypoint}" was not found.`,
|
|
5654
5552
|
link: PYTHON_ENTRYPOINT_DOCS_URL,
|
|
@@ -5658,17 +5556,17 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
|
|
|
5658
5556
|
}
|
|
5659
5557
|
let varName = configEntryVar ?? await checkEntrypoint(workPath, entrypoint);
|
|
5660
5558
|
if (!varName) {
|
|
5661
|
-
const needsDynamicApp = !!service && ((0,
|
|
5559
|
+
const needsDynamicApp = !!service && ((0, import_build_utils7.isScheduleTriggeredService)(service) || (0, import_build_utils7.isQueueTriggeredService)(service));
|
|
5662
5560
|
if (needsDynamicApp) {
|
|
5663
5561
|
varName = "app";
|
|
5664
5562
|
}
|
|
5665
5563
|
}
|
|
5666
5564
|
if (varName) {
|
|
5667
|
-
(0,
|
|
5565
|
+
(0, import_build_utils8.debug)(`Using configured Python entrypoint: ${entrypoint}`);
|
|
5668
5566
|
return { entrypoint: { entrypoint, variableName: varName } };
|
|
5669
5567
|
} else {
|
|
5670
5568
|
return {
|
|
5671
|
-
error: new
|
|
5569
|
+
error: new import_build_utils7.NowBuildError({
|
|
5672
5570
|
code: "PYTHON_ENTRYPOINT_NOT_FOUND",
|
|
5673
5571
|
message: `Could not find a top-level "app", "application", or "handler" in "${entrypoint}".`,
|
|
5674
5572
|
link: PYTHON_ENTRYPOINT_DOCS_URL,
|
|
@@ -5684,36 +5582,18 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
|
|
|
5684
5582
|
if (vercelEntry)
|
|
5685
5583
|
return { entrypoint: vercelEntry };
|
|
5686
5584
|
let findDiagnostics;
|
|
5687
|
-
let
|
|
5585
|
+
let djangoManageBaseDir;
|
|
5688
5586
|
if (framework === "django") {
|
|
5689
5587
|
const djangoResult = await detectDjangoPythonEntrypoint(workPath);
|
|
5690
5588
|
findDiagnostics = djangoResult.findDiagnostics;
|
|
5691
|
-
searchDirs = djangoResult.dirs;
|
|
5692
5589
|
if (djangoResult.detected) {
|
|
5693
5590
|
if (djangoResult.detected.entrypoint)
|
|
5694
5591
|
return djangoResult.detected;
|
|
5695
|
-
|
|
5696
|
-
const otherPyFiles2 = await findOtherPyFiles(
|
|
5697
|
-
workPath,
|
|
5698
|
-
searchDirs,
|
|
5699
|
-
candidateSet2
|
|
5700
|
-
);
|
|
5701
|
-
const pyprojectResult2 = await getPyprojectEntrypointWithDiagnostics(workPath);
|
|
5702
|
-
const diagnostics3 = {
|
|
5703
|
-
existingWithoutEntrypoint: findDiagnostics.existingWithoutEntrypoint,
|
|
5704
|
-
otherPyFiles: otherPyFiles2,
|
|
5705
|
-
pyprojectAppScript: pyprojectResult2.appScript,
|
|
5706
|
-
pyprojectCheckedPaths: pyprojectResult2.checkedPaths
|
|
5707
|
-
};
|
|
5708
|
-
return {
|
|
5709
|
-
baseDir: djangoResult.detected.baseDir,
|
|
5710
|
-
error: makeDiagnosticError("django", diagnostics3)
|
|
5711
|
-
};
|
|
5592
|
+
djangoManageBaseDir = djangoResult.detected.baseDir;
|
|
5712
5593
|
}
|
|
5713
5594
|
} else {
|
|
5714
5595
|
const genericResult = await detectGenericPythonEntrypoint(workPath);
|
|
5715
5596
|
findDiagnostics = genericResult.findDiagnostics;
|
|
5716
|
-
searchDirs = PYTHON_ENTRYPOINT_DIRS;
|
|
5717
5597
|
if (genericResult.detected)
|
|
5718
5598
|
return genericResult.detected;
|
|
5719
5599
|
}
|
|
@@ -5721,22 +5601,165 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
|
|
|
5721
5601
|
if (pyprojectResult.entrypoint) {
|
|
5722
5602
|
return { entrypoint: pyprojectResult.entrypoint };
|
|
5723
5603
|
}
|
|
5724
|
-
const
|
|
5725
|
-
const otherPyFiles = await findOtherPyFiles(
|
|
5726
|
-
workPath,
|
|
5727
|
-
searchDirs,
|
|
5728
|
-
candidateSet
|
|
5729
|
-
);
|
|
5604
|
+
const validCandidatesElsewhere = await findEntrypointCandidates(workPath);
|
|
5730
5605
|
const diagnostics2 = {
|
|
5731
5606
|
existingWithoutEntrypoint: findDiagnostics.existingWithoutEntrypoint,
|
|
5732
|
-
otherPyFiles,
|
|
5733
5607
|
pyprojectAppScript: pyprojectResult.appScript,
|
|
5734
|
-
pyprojectCheckedPaths: pyprojectResult.checkedPaths
|
|
5608
|
+
pyprojectCheckedPaths: pyprojectResult.checkedPaths,
|
|
5609
|
+
validCandidatesElsewhere,
|
|
5610
|
+
djangoManageBaseDir
|
|
5735
5611
|
};
|
|
5736
|
-
|
|
5612
|
+
const error = makeDiagnosticError(framework, diagnostics2);
|
|
5613
|
+
return djangoManageBaseDir !== void 0 ? { baseDir: djangoManageBaseDir, error } : { error };
|
|
5614
|
+
}
|
|
5615
|
+
|
|
5616
|
+
// src/crons.ts
|
|
5617
|
+
var DYNAMIC_SCHEDULE = "<dynamic>";
|
|
5618
|
+
var scriptPath = (0, import_path8.join)(__dirname, "..", "templates", "vc_cron_detect.py");
|
|
5619
|
+
var script = import_fs8.default.readFileSync(scriptPath, "utf-8");
|
|
5620
|
+
function buildCronRouteTable(crons) {
|
|
5621
|
+
const table = {};
|
|
5622
|
+
for (const cron of crons) {
|
|
5623
|
+
table[cron.path] = cron.resolvedHandler;
|
|
5624
|
+
}
|
|
5625
|
+
return table;
|
|
5626
|
+
}
|
|
5627
|
+
async function getServiceCrons(opts) {
|
|
5628
|
+
const { service, entrypoint, rawEntrypoint, handlerFunction } = opts;
|
|
5629
|
+
const isScheduledService = !!service && (0, import_build_utils10.isScheduleTriggeredService)(service);
|
|
5630
|
+
if (!isScheduledService || !service.name || typeof service.schedule !== "string" && !Array.isArray(service.schedule)) {
|
|
5631
|
+
return void 0;
|
|
5632
|
+
}
|
|
5633
|
+
const cronEntrypoint = entrypoint || rawEntrypoint;
|
|
5634
|
+
if (!cronEntrypoint) {
|
|
5635
|
+
throw new import_build_utils10.NowBuildError({
|
|
5636
|
+
code: "PYTHON_CRON_NO_ENTRYPOINT",
|
|
5637
|
+
message: "Cron service is missing an entrypoint."
|
|
5638
|
+
});
|
|
5639
|
+
}
|
|
5640
|
+
if (service.schedule === DYNAMIC_SCHEDULE) {
|
|
5641
|
+
return getServiceCronsDynamic({
|
|
5642
|
+
serviceName: service.name,
|
|
5643
|
+
cronEntrypoint,
|
|
5644
|
+
handlerFunction,
|
|
5645
|
+
pythonBin: opts.pythonBin,
|
|
5646
|
+
env: opts.env,
|
|
5647
|
+
workPath: opts.workPath
|
|
5648
|
+
});
|
|
5649
|
+
}
|
|
5650
|
+
const cronPath = (0, import_build_utils10.getInternalServiceCronPath)(
|
|
5651
|
+
service.name,
|
|
5652
|
+
cronEntrypoint,
|
|
5653
|
+
handlerFunction || "cron"
|
|
5654
|
+
);
|
|
5655
|
+
const moduleName = entrypointToModule(cronEntrypoint);
|
|
5656
|
+
const resolvedHandler = handlerFunction ? `${moduleName}:${handlerFunction}` : moduleName;
|
|
5657
|
+
const schedules = Array.isArray(service.schedule) ? service.schedule : [service.schedule];
|
|
5658
|
+
return schedules.map((schedule) => ({
|
|
5659
|
+
path: cronPath,
|
|
5660
|
+
schedule,
|
|
5661
|
+
resolvedHandler
|
|
5662
|
+
}));
|
|
5663
|
+
}
|
|
5664
|
+
async function getServiceCronsDynamic(opts) {
|
|
5665
|
+
const {
|
|
5666
|
+
serviceName,
|
|
5667
|
+
cronEntrypoint,
|
|
5668
|
+
handlerFunction,
|
|
5669
|
+
pythonBin,
|
|
5670
|
+
env,
|
|
5671
|
+
workPath
|
|
5672
|
+
} = opts;
|
|
5673
|
+
if (!handlerFunction) {
|
|
5674
|
+
throw new import_build_utils10.NowBuildError({
|
|
5675
|
+
code: "PYTHON_DYNAMIC_CRON_NO_HANDLER",
|
|
5676
|
+
message: 'Dynamic cron detection requires a "module:object" entrypoint where the object has a get_crons() method.'
|
|
5677
|
+
});
|
|
5678
|
+
}
|
|
5679
|
+
const moduleName = entrypointToModule(cronEntrypoint);
|
|
5680
|
+
const entries = await detectDynamicCrons({
|
|
5681
|
+
pythonBin,
|
|
5682
|
+
env,
|
|
5683
|
+
workPath,
|
|
5684
|
+
moduleName,
|
|
5685
|
+
attrName: handlerFunction
|
|
5686
|
+
});
|
|
5687
|
+
console.log(
|
|
5688
|
+
`Detected ${entries.length} cron entry(s): ${entries.map((e) => `${e.module_function} (${e.schedule})`).join(", ")}`
|
|
5689
|
+
);
|
|
5690
|
+
if (entries.length === 0) {
|
|
5691
|
+
throw new import_build_utils10.NowBuildError({
|
|
5692
|
+
code: "PYTHON_DYNAMIC_CRON_EMPTY",
|
|
5693
|
+
message: `Dynamic cron detection returned no entries. "${moduleName}.${handlerFunction}.get_crons()" returned an empty iterable.`
|
|
5694
|
+
});
|
|
5695
|
+
}
|
|
5696
|
+
return entries.map((entry) => {
|
|
5697
|
+
const cronPath = moduleColonFuncToCronPath(
|
|
5698
|
+
serviceName,
|
|
5699
|
+
entry.module_function
|
|
5700
|
+
);
|
|
5701
|
+
return {
|
|
5702
|
+
path: cronPath,
|
|
5703
|
+
schedule: entry.schedule,
|
|
5704
|
+
resolvedHandler: entry.module_function
|
|
5705
|
+
};
|
|
5706
|
+
});
|
|
5707
|
+
}
|
|
5708
|
+
async function detectDynamicCrons(opts) {
|
|
5709
|
+
const { pythonBin, env, workPath, moduleName, attrName } = opts;
|
|
5710
|
+
let stdout;
|
|
5711
|
+
try {
|
|
5712
|
+
const result = await (0, import_execa4.default)(
|
|
5713
|
+
pythonBin,
|
|
5714
|
+
["-c", script, moduleName, attrName],
|
|
5715
|
+
{ env, cwd: workPath }
|
|
5716
|
+
);
|
|
5717
|
+
stdout = result.stdout;
|
|
5718
|
+
} catch (err) {
|
|
5719
|
+
let detail = err?.stderr || err?.message || String(err);
|
|
5720
|
+
try {
|
|
5721
|
+
const parsed2 = JSON.parse(err?.stdout);
|
|
5722
|
+
if (parsed2.error)
|
|
5723
|
+
detail = parsed2.error;
|
|
5724
|
+
} catch {
|
|
5725
|
+
}
|
|
5726
|
+
throw new import_build_utils10.NowBuildError({
|
|
5727
|
+
code: "PYTHON_DYNAMIC_CRON_DETECTION_FAILED",
|
|
5728
|
+
message: `Failed to detect dynamic cron entries: ${detail}`
|
|
5729
|
+
});
|
|
5730
|
+
}
|
|
5731
|
+
let parsed;
|
|
5732
|
+
try {
|
|
5733
|
+
parsed = JSON.parse(stdout);
|
|
5734
|
+
} catch {
|
|
5735
|
+
throw new import_build_utils10.NowBuildError({
|
|
5736
|
+
code: "PYTHON_DYNAMIC_CRON_DETECTION_FAILED",
|
|
5737
|
+
message: `Dynamic cron detection returned invalid JSON: ${stdout}`
|
|
5738
|
+
});
|
|
5739
|
+
}
|
|
5740
|
+
return parsed.entries || [];
|
|
5741
|
+
}
|
|
5742
|
+
function moduleColonFuncToCronPath(serviceName, moduleFunction) {
|
|
5743
|
+
const colonIdx = moduleFunction.indexOf(":");
|
|
5744
|
+
if (colonIdx === -1) {
|
|
5745
|
+
throw new import_build_utils10.NowBuildError({
|
|
5746
|
+
code: "PYTHON_DYNAMIC_CRON_INVALID_FORMAT",
|
|
5747
|
+
message: `Dynamic cron entry must use "module:function" format, got: "${moduleFunction}"`
|
|
5748
|
+
});
|
|
5749
|
+
}
|
|
5750
|
+
const modulePart = moduleFunction.slice(0, colonIdx);
|
|
5751
|
+
const funcPart = moduleFunction.slice(colonIdx + 1);
|
|
5752
|
+
const entrypointPath = modulePart.replace(/\./g, "/");
|
|
5753
|
+
return (0, import_build_utils10.getInternalServiceCronPath)(serviceName, entrypointPath, funcPart);
|
|
5737
5754
|
}
|
|
5738
5755
|
|
|
5739
5756
|
// src/start-dev-server.ts
|
|
5757
|
+
var import_child_process2 = require("child_process");
|
|
5758
|
+
var import_fs9 = require("fs");
|
|
5759
|
+
var import_path9 = require("path");
|
|
5760
|
+
var import_build_utils11 = require("@vercel/build-utils");
|
|
5761
|
+
var import_get_port = __toESM(require_get_port());
|
|
5762
|
+
var import_is_port_reachable = __toESM(require_is_port_reachable());
|
|
5740
5763
|
var import_python_analysis6 = require("@vercel/python-analysis");
|
|
5741
5764
|
var DEV_SERVER_STARTUP_TIMEOUT = 5 * 6e4;
|
|
5742
5765
|
function silenceNodeWarnings() {
|
|
@@ -6313,7 +6336,7 @@ var startDevServer = async (opts) => {
|
|
|
6313
6336
|
});
|
|
6314
6337
|
}
|
|
6315
6338
|
const { entrypoint: entry, variableName } = resolved;
|
|
6316
|
-
const modulePath = entry
|
|
6339
|
+
const modulePath = entrypointToModule(entry);
|
|
6317
6340
|
let childProcess = null;
|
|
6318
6341
|
let stdoutLogListener = null;
|
|
6319
6342
|
let stderrLogListener = null;
|
|
@@ -7442,7 +7465,7 @@ var build = async ({
|
|
|
7442
7465
|
await uv.pip({
|
|
7443
7466
|
venvPath,
|
|
7444
7467
|
projectDir: (0, import_path13.join)(workPath, entryDirectory),
|
|
7445
|
-
args: ["install", runtimeDep]
|
|
7468
|
+
args: ["install", "--link-mode", "copy", runtimeDep]
|
|
7446
7469
|
});
|
|
7447
7470
|
if (shouldInstallVercelWorkers) {
|
|
7448
7471
|
const workersDep = baseEnv.VERCEL_WORKERS_PYTHON || `vercel-workers==${VERCEL_WORKERS_VERSION}`;
|
|
@@ -7450,7 +7473,7 @@ var build = async ({
|
|
|
7450
7473
|
await uv.pip({
|
|
7451
7474
|
venvPath,
|
|
7452
7475
|
projectDir: (0, import_path13.join)(workPath, entryDirectory),
|
|
7453
|
-
args: ["install", workersDep]
|
|
7476
|
+
args: ["install", "--link-mode", "copy", workersDep]
|
|
7454
7477
|
});
|
|
7455
7478
|
}
|
|
7456
7479
|
const quirksResult = await runQuirks({ venvPath, pythonEnv, workPath });
|
|
@@ -7458,7 +7481,7 @@ var build = async ({
|
|
|
7458
7481
|
Object.assign(pythonEnv, quirksResult.buildEnv);
|
|
7459
7482
|
}
|
|
7460
7483
|
(0, import_build_utils16.debug)("Entrypoint is", entrypoint);
|
|
7461
|
-
const moduleName = entrypoint
|
|
7484
|
+
const moduleName = entrypointToModule(entrypoint);
|
|
7462
7485
|
if (handlerFunction) {
|
|
7463
7486
|
const entrypointPath = (0, import_path13.join)(workPath, entrypoint);
|
|
7464
7487
|
const source = await import_fs13.default.promises.readFile(entrypointPath, "utf-8");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/python",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.41.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"smol-toml": "1.5.2",
|
|
35
35
|
"vitest": "2.1.4",
|
|
36
36
|
"which": "3.0.0",
|
|
37
|
-
"@vercel/build-utils": "13.
|
|
38
|
-
"@vercel/
|
|
39
|
-
"@vercel/
|
|
37
|
+
"@vercel/build-utils": "13.24.0",
|
|
38
|
+
"@vercel/python-runtime": "0.13.2",
|
|
39
|
+
"@vercel/error-utils": "2.1.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "node ../../utils/build-builder.mjs",
|