@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.
Files changed (2) hide show
  1. package/dist/index.js +259 -236
  2. 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 import_fs7 = __toESM(require("fs"));
5219
- var import_path7 = require("path");
5218
+ var import_fs8 = __toESM(require("fs"));
5219
+ var import_path8 = require("path");
5220
5220
  var import_execa4 = __toESM(require_execa());
5221
- var import_build_utils7 = require("@vercel/build-utils");
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 import_fs8 = __toESM(require("fs"));
5374
- var import_path8 = require("path");
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) => import_path8.posix.join(dir, `${filename}.py`)
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 import_fs8.default.promises.stat(filePath);
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, import_path8.join)(workPath, relPath);
5308
+ const absPath = (0, import_path7.join)(workPath, relPath);
5454
5309
  if (!await fileExists(absPath))
5455
5310
  return null;
5456
- const content = await import_fs8.default.promises.readFile(absPath, "utf-8");
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 findOtherPyFiles(workPath, dirs, candidateSet) {
5314
+ async function findPythonFilesRecursive(dir, rootDir) {
5460
5315
  const results = [];
5461
- for (const dir of dirs) {
5462
- const dirPath = (0, import_path8.join)(workPath, dir);
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 entries = await import_fs8.default.promises.readdir(dirPath);
5465
- for (const entry of entries) {
5466
- if (!entry.endsWith(".py"))
5467
- continue;
5468
- const relPath = dir ? import_path8.posix.join(dir, entry) : entry;
5469
- if (candidateSet.has(relPath))
5470
- continue;
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 results;
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, import_path8.join)(workPath, candidate))) {
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, import_build_utils10.readConfigFile)((0, import_path8.join)(workPath, "pyproject.toml"));
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, import_build_utils10.readConfigFile)((0, import_path8.join)(workPath, "pyproject.toml"));
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, import_path8.join)(workPath, candidate))) {
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, import_path8.join)(workPath, candidate);
5415
+ const absPath = (0, import_path7.join)(workPath, candidate);
5531
5416
  if (!await fileExists(absPath))
5532
5417
  continue;
5533
- const content = await import_fs8.default.promises.readFile(absPath, "utf-8");
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, import_build_utils9.debug)(`Detected Python entrypoint: ${candidate} (variable: ${varName})`);
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, import_path8.join)(workPath, "manage.py");
5432
+ const managePath = (0, import_path7.join)(workPath, "manage.py");
5548
5433
  try {
5549
- const content = await import_fs8.default.promises.readFile(managePath, "utf-8");
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, import_build_utils9.debug)(`Found Django manage.py with DJANGO_SETTINGS_MODULE at ${workPath}`);
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 import_fs8.default.promises.readdir(workPath, {
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
- } else if (diagnostics2.otherPyFiles.length > 0) {
5583
- const fileList = diagnostics2.otherPyFiles.slice(0, 5).join(", ");
5584
- message = `No ${displayName} entrypoint found in standard locations. Found Python files: ${fileList}. Rename one to app.py or set "tool.vercel.entrypoint" in pyproject.toml.`;
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 import_build_utils8.NowBuildError({ code, message, link, action });
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, import_build_utils9.debug)("Failed to discover Python entrypoint");
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, import_path8.join)(workPath, rootDir);
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, import_build_utils9.debug)("Failed to discover Django Python entrypoint");
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, import_path8.join)(workPath, entrypoint));
5546
+ const entrypointExists = await fileExists((0, import_path7.join)(workPath, entrypoint));
5649
5547
  if (!entrypointExists) {
5650
5548
  return {
5651
- error: new import_build_utils8.NowBuildError({
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, import_build_utils8.isScheduleTriggeredService)(service) || (0, import_build_utils8.isQueueTriggeredService)(service));
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, import_build_utils9.debug)(`Using configured Python entrypoint: ${entrypoint}`);
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 import_build_utils8.NowBuildError({
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 searchDirs;
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
- const candidateSet2 = new Set(getCandidateEntrypointsInDirs(searchDirs));
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 candidateSet = new Set(getCandidateEntrypointsInDirs(searchDirs));
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
- return { error: makeDiagnosticError(framework, diagnostics2) };
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.replace(/\.py$/i, "").replace(/[\\/]/g, ".");
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.replace(/\//g, ".").replace(/\.py$/i, "");
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.40.0",
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.23.0",
38
- "@vercel/error-utils": "2.1.0",
39
- "@vercel/python-runtime": "0.13.2"
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",