@vercel/python 6.40.0 → 6.42.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 +298 -238
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3213,6 +3213,7 @@ var src_exports = {};
3213
3213
  __export(src_exports, {
3214
3214
  build: () => build,
3215
3215
  defaultShouldServe: () => defaultShouldServe,
3216
+ detectEntrypoint: () => detectEntrypoint,
3216
3217
  diagnostics: () => diagnostics,
3217
3218
  downloadFilesInWorkPath: () => downloadFilesInWorkPath,
3218
3219
  installRequirement: () => installRequirement,
@@ -3231,7 +3232,7 @@ var import_path13 = require("path");
3231
3232
 
3232
3233
  // src/package-versions.ts
3233
3234
  var VERCEL_RUNTIME_VERSION = "0.13.2";
3234
- var VERCEL_WORKERS_VERSION = "0.0.22";
3235
+ var VERCEL_WORKERS_VERSION = "0.0.23";
3235
3236
 
3236
3237
  // src/index.ts
3237
3238
  var import_build_utils16 = require("@vercel/build-utils");
@@ -5215,166 +5216,17 @@ async function generateProjectManifest({
5215
5216
  var diagnostics = (0, import_build_utils6.createDiagnostics)("python");
5216
5217
 
5217
5218
  // src/crons.ts
5218
- var import_fs7 = __toESM(require("fs"));
5219
- var import_path7 = require("path");
5219
+ var import_fs8 = __toESM(require("fs"));
5220
+ var import_path8 = require("path");
5220
5221
  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());
5222
+ var import_build_utils10 = require("@vercel/build-utils");
5371
5223
 
5372
5224
  // src/entrypoint.ts
5373
- var import_fs8 = __toESM(require("fs"));
5374
- var import_path8 = require("path");
5225
+ var import_fs7 = __toESM(require("fs"));
5226
+ var import_path7 = require("path");
5227
+ var import_build_utils7 = require("@vercel/build-utils");
5375
5228
  var import_build_utils8 = require("@vercel/build-utils");
5376
5229
  var import_build_utils9 = require("@vercel/build-utils");
5377
- var import_build_utils10 = require("@vercel/build-utils");
5378
5230
  var import_python_analysis5 = require("@vercel/python-analysis");
5379
5231
  var PYTHON_ENTRYPOINT_FILENAMES = [
5380
5232
  "app",
@@ -5389,16 +5241,17 @@ var PYTHON_CANDIDATE_ENTRYPOINTS = getCandidateEntrypointsInDirs(
5389
5241
  PYTHON_ENTRYPOINT_DIRS
5390
5242
  );
5391
5243
  var PYTHON_ENTRYPOINT_DOCS_URL = "https://vercel.com/docs/functions/runtimes/python#python-entrypoints";
5244
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["__pycache__", "node_modules"]);
5392
5245
  function getCandidateEntrypointsInDirs(dirs) {
5393
5246
  return dirs.flatMap(
5394
5247
  (dir) => PYTHON_ENTRYPOINT_FILENAMES.map(
5395
- (filename) => import_path8.posix.join(dir, `${filename}.py`)
5248
+ (filename) => import_path7.posix.join(dir, `${filename}.py`)
5396
5249
  )
5397
5250
  );
5398
5251
  }
5399
5252
  async function fileExists(filePath) {
5400
5253
  try {
5401
- const stat = await import_fs8.default.promises.stat(filePath);
5254
+ const stat = await import_fs7.default.promises.stat(filePath);
5402
5255
  return stat.isFile();
5403
5256
  } catch {
5404
5257
  return false;
@@ -5449,34 +5302,67 @@ function getMissingExportMessage(framework, entrypoints) {
5449
5302
  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
5303
  }
5451
5304
  }
5305
+ function entrypointToModule(entrypoint) {
5306
+ return entrypoint.replace(/\\/g, "/").replace(/\.py$/i, "").replace(/\//g, ".");
5307
+ }
5452
5308
  async function checkEntrypoint(workPath, relPath) {
5453
- const absPath = (0, import_path8.join)(workPath, relPath);
5309
+ const absPath = (0, import_path7.join)(workPath, relPath);
5454
5310
  if (!await fileExists(absPath))
5455
5311
  return null;
5456
- const content = await import_fs8.default.promises.readFile(absPath, "utf-8");
5312
+ const content = await import_fs7.default.promises.readFile(absPath, "utf-8");
5457
5313
  return (0, import_python_analysis5.findAppOrHandler)(content);
5458
5314
  }
5459
- async function findOtherPyFiles(workPath, dirs, candidateSet) {
5315
+ async function findPythonFilesRecursive(dir, rootDir) {
5460
5316
  const results = [];
5461
- for (const dir of dirs) {
5462
- const dirPath = (0, import_path8.join)(workPath, dir);
5317
+ let entries;
5318
+ try {
5319
+ entries = await import_fs7.default.promises.readdir(dir, { withFileTypes: true });
5320
+ } catch {
5321
+ return results;
5322
+ }
5323
+ for (const entry of entries) {
5324
+ if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name))
5325
+ continue;
5326
+ const fullPath = (0, import_path7.join)(dir, entry.name);
5327
+ if (entry.isDirectory()) {
5328
+ results.push(...await findPythonFilesRecursive(fullPath, rootDir));
5329
+ } else if (entry.isFile() && entry.name.endsWith(".py")) {
5330
+ results.push((0, import_path7.relative)(rootDir, fullPath).replace(/\\/g, "/"));
5331
+ }
5332
+ }
5333
+ return results;
5334
+ }
5335
+ async function findEntrypointCandidates(workPath) {
5336
+ const pyFiles = await findPythonFilesRecursive(workPath, workPath);
5337
+ const candidates = [];
5338
+ for (const relPath of pyFiles) {
5463
5339
  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
- }
5340
+ const content = await import_fs7.default.promises.readFile(
5341
+ (0, import_path7.join)(workPath, relPath),
5342
+ "utf-8"
5343
+ );
5344
+ const varName = await (0, import_python_analysis5.findAppOrHandler)(content);
5345
+ if (varName) {
5346
+ candidates.push({ entrypoint: relPath, variableName: varName });
5475
5347
  }
5476
5348
  } catch {
5477
5349
  }
5478
5350
  }
5479
- return results;
5351
+ return candidates;
5352
+ }
5353
+ function renderCandidateSuggestion(candidates) {
5354
+ const lines = candidates.map(
5355
+ (c) => ` ${c.entrypoint} (variable: ${c.variableName})`
5356
+ );
5357
+ const suggested = candidates[0];
5358
+ const moduleAttr = `${entrypointToModule(suggested.entrypoint)}:${suggested.variableName}`;
5359
+ return `but found potential entrypoints:
5360
+ ${lines.join("\n")}
5361
+
5362
+ Add this to your pyproject.toml:
5363
+
5364
+ [tool.vercel]
5365
+ entrypoint = "${moduleAttr}"`;
5480
5366
  }
5481
5367
  async function resolveModuleAttrEntrypoint(workPath, value) {
5482
5368
  const match = value.match(/([A-Za-z_][\w.]*)\s*:\s*([A-Za-z_][\w]*)/);
@@ -5487,14 +5373,14 @@ async function resolveModuleAttrEntrypoint(workPath, value) {
5487
5373
  const relPath = modulePath.replace(/\./g, "/");
5488
5374
  const candidates = [`${relPath}.py`, `${relPath}/__init__.py`];
5489
5375
  for (const candidate of candidates) {
5490
- if (await fileExists((0, import_path8.join)(workPath, candidate))) {
5376
+ if (await fileExists((0, import_path7.join)(workPath, candidate))) {
5491
5377
  return { entrypoint: candidate, variableName };
5492
5378
  }
5493
5379
  }
5494
5380
  return null;
5495
5381
  }
5496
5382
  async function getVercelToolsEntrypoint(workPath) {
5497
- const pyprojectData = await (0, import_build_utils10.readConfigFile)((0, import_path8.join)(workPath, "pyproject.toml"));
5383
+ const pyprojectData = await (0, import_build_utils9.readConfigFile)((0, import_path7.join)(workPath, "pyproject.toml"));
5498
5384
  if (!pyprojectData)
5499
5385
  return null;
5500
5386
  const vercelEntrypoint = pyprojectData.tool?.vercel?.entrypoint;
@@ -5503,7 +5389,7 @@ async function getVercelToolsEntrypoint(workPath) {
5503
5389
  return resolveModuleAttrEntrypoint(workPath, vercelEntrypoint);
5504
5390
  }
5505
5391
  async function getPyprojectEntrypointWithDiagnostics(workPath) {
5506
- const pyprojectData = await (0, import_build_utils10.readConfigFile)((0, import_path8.join)(workPath, "pyproject.toml"));
5392
+ const pyprojectData = await (0, import_build_utils9.readConfigFile)((0, import_path7.join)(workPath, "pyproject.toml"));
5507
5393
  if (!pyprojectData)
5508
5394
  return { entrypoint: null };
5509
5395
  const scripts = pyprojectData.project?.scripts;
@@ -5518,7 +5404,7 @@ async function getPyprojectEntrypointWithDiagnostics(workPath) {
5518
5404
  const relPath = modulePath.replace(/\./g, "/");
5519
5405
  const candidates = [`${relPath}.py`, `${relPath}/__init__.py`];
5520
5406
  for (const candidate of candidates) {
5521
- if (await fileExists((0, import_path8.join)(workPath, candidate))) {
5407
+ if (await fileExists((0, import_path7.join)(workPath, candidate))) {
5522
5408
  return { entrypoint: { entrypoint: candidate, variableName } };
5523
5409
  }
5524
5410
  }
@@ -5527,13 +5413,13 @@ async function getPyprojectEntrypointWithDiagnostics(workPath) {
5527
5413
  async function findValidEntrypoint(workPath, candidates) {
5528
5414
  const existingWithoutEntrypoint = [];
5529
5415
  for (const candidate of candidates) {
5530
- const absPath = (0, import_path8.join)(workPath, candidate);
5416
+ const absPath = (0, import_path7.join)(workPath, candidate);
5531
5417
  if (!await fileExists(absPath))
5532
5418
  continue;
5533
- const content = await import_fs8.default.promises.readFile(absPath, "utf-8");
5419
+ const content = await import_fs7.default.promises.readFile(absPath, "utf-8");
5534
5420
  const varName = await (0, import_python_analysis5.findAppOrHandler)(content);
5535
5421
  if (varName) {
5536
- (0, import_build_utils9.debug)(`Detected Python entrypoint: ${candidate} (variable: ${varName})`);
5422
+ (0, import_build_utils8.debug)(`Detected Python entrypoint: ${candidate} (variable: ${varName})`);
5537
5423
  return {
5538
5424
  entrypoint: { entrypoint: candidate, variableName: varName },
5539
5425
  existingWithoutEntrypoint
@@ -5544,12 +5430,12 @@ async function findValidEntrypoint(workPath, candidates) {
5544
5430
  return { entrypoint: null, existingWithoutEntrypoint };
5545
5431
  }
5546
5432
  async function checkDjangoManage(workPath) {
5547
- const managePath = (0, import_path8.join)(workPath, "manage.py");
5433
+ const managePath = (0, import_path7.join)(workPath, "manage.py");
5548
5434
  try {
5549
- const content = await import_fs8.default.promises.readFile(managePath, "utf-8");
5435
+ const content = await import_fs7.default.promises.readFile(managePath, "utf-8");
5550
5436
  if (!content.includes("DJANGO_SETTINGS_MODULE"))
5551
5437
  return false;
5552
- (0, import_build_utils9.debug)(`Found Django manage.py with DJANGO_SETTINGS_MODULE at ${workPath}`);
5438
+ (0, import_build_utils8.debug)(`Found Django manage.py with DJANGO_SETTINGS_MODULE at ${workPath}`);
5553
5439
  return true;
5554
5440
  } catch {
5555
5441
  return false;
@@ -5557,7 +5443,7 @@ async function checkDjangoManage(workPath) {
5557
5443
  }
5558
5444
  async function getSubdirectories(workPath) {
5559
5445
  try {
5560
- const entries = await import_fs8.default.promises.readdir(workPath, {
5446
+ const entries = await import_fs7.default.promises.readdir(workPath, {
5561
5447
  withFileTypes: true
5562
5448
  });
5563
5449
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
@@ -5570,23 +5456,39 @@ function makeDiagnosticError(framework, diagnostics2) {
5570
5456
  const link = getFrameworkEntrypointDocsUrl(framework);
5571
5457
  const action = "Learn More";
5572
5458
  const displayName = getFrameworkDisplayName(framework);
5459
+ const elsewhere = diagnostics2.validCandidatesElsewhere ?? [];
5460
+ const suggestion = elsewhere.length > 0 ? renderCandidateSuggestion(elsewhere) : null;
5573
5461
  let message;
5574
5462
  if (diagnostics2.existingWithoutEntrypoint.length > 0) {
5575
5463
  message = getMissingExportMessage(
5576
5464
  framework,
5577
5465
  diagnostics2.existingWithoutEntrypoint
5578
5466
  );
5467
+ if (suggestion)
5468
+ message += `
5469
+
5470
+ ${suggestion}`;
5471
+ } else if (diagnostics2.djangoManageBaseDir !== void 0) {
5472
+ const where = diagnostics2.djangoManageBaseDir || ".";
5473
+ 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.`;
5474
+ if (suggestion)
5475
+ message += `
5476
+
5477
+ ${suggestion}`;
5579
5478
  } else if (diagnostics2.pyprojectAppScript && diagnostics2.pyprojectCheckedPaths?.length) {
5580
5479
  const checked = diagnostics2.pyprojectCheckedPaths.join(" or ");
5581
5480
  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.`;
5481
+ if (suggestion)
5482
+ message += `
5483
+
5484
+ ${suggestion}`;
5485
+ } else if (suggestion) {
5486
+ message = `No ${displayName} entrypoint found in default locations, ${suggestion}`;
5585
5487
  } else {
5586
5488
  const searchedList = PYTHON_CANDIDATE_ENTRYPOINTS.join(", ");
5587
5489
  message = `No ${displayName} entrypoint found. Set "tool.vercel.entrypoint" in pyproject.toml or define an entrypoint in one of: ${searchedList}.`;
5588
5490
  }
5589
- return new import_build_utils8.NowBuildError({ code, message, link, action });
5491
+ return new import_build_utils7.NowBuildError({ code, message, link, action });
5590
5492
  }
5591
5493
  async function detectGenericPythonEntrypoint(workPath) {
5592
5494
  try {
@@ -5599,7 +5501,7 @@ async function detectGenericPythonEntrypoint(workPath) {
5599
5501
  findDiagnostics: result
5600
5502
  };
5601
5503
  } catch {
5602
- (0, import_build_utils9.debug)("Failed to discover Python entrypoint");
5504
+ (0, import_build_utils8.debug)("Failed to discover Python entrypoint");
5603
5505
  return {
5604
5506
  detected: null,
5605
5507
  findDiagnostics: { entrypoint: null, existingWithoutEntrypoint: [] }
@@ -5612,20 +5514,18 @@ async function detectDjangoPythonEntrypoint(workPath) {
5612
5514
  findDiagnostics: {
5613
5515
  entrypoint: null,
5614
5516
  existingWithoutEntrypoint: []
5615
- },
5616
- dirs: PYTHON_ENTRYPOINT_DIRS
5517
+ }
5617
5518
  };
5618
5519
  try {
5619
5520
  const subdirs = await getSubdirectories(workPath);
5620
5521
  const rootDirs = ["", ...subdirs];
5621
5522
  for (const rootDir of rootDirs) {
5622
- const currPath = (0, import_path8.join)(workPath, rootDir);
5523
+ const currPath = (0, import_path7.join)(workPath, rootDir);
5623
5524
  const isDjango = await checkDjangoManage(currPath);
5624
5525
  if (isDjango) {
5625
5526
  return {
5626
5527
  detected: { baseDir: rootDir },
5627
- findDiagnostics: { entrypoint: null, existingWithoutEntrypoint: [] },
5628
- dirs: rootDirs
5528
+ findDiagnostics: { entrypoint: null, existingWithoutEntrypoint: [] }
5629
5529
  };
5630
5530
  }
5631
5531
  }
@@ -5633,11 +5533,10 @@ async function detectDjangoPythonEntrypoint(workPath) {
5633
5533
  const result = await findValidEntrypoint(workPath, candidates);
5634
5534
  return {
5635
5535
  detected: result.entrypoint ? { entrypoint: result.entrypoint } : null,
5636
- findDiagnostics: result,
5637
- dirs: rootDirs
5536
+ findDiagnostics: result
5638
5537
  };
5639
5538
  } catch {
5640
- (0, import_build_utils9.debug)("Failed to discover Django Python entrypoint");
5539
+ (0, import_build_utils8.debug)("Failed to discover Django Python entrypoint");
5641
5540
  return emptyResult;
5642
5541
  }
5643
5542
  }
@@ -5645,10 +5544,10 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
5645
5544
  if (configuredEntrypoint) {
5646
5545
  const { filePath: configEntryFile, varName: configEntryVar } = configuredEntrypoint;
5647
5546
  const entrypoint = configEntryFile.endsWith(".py") ? configEntryFile : `${configEntryFile}.py`;
5648
- const entrypointExists = await fileExists((0, import_path8.join)(workPath, entrypoint));
5547
+ const entrypointExists = await fileExists((0, import_path7.join)(workPath, entrypoint));
5649
5548
  if (!entrypointExists) {
5650
5549
  return {
5651
- error: new import_build_utils8.NowBuildError({
5550
+ error: new import_build_utils7.NowBuildError({
5652
5551
  code: "PYTHON_ENTRYPOINT_NOT_FOUND",
5653
5552
  message: `Configured Python entrypoint "${entrypoint}" was not found.`,
5654
5553
  link: PYTHON_ENTRYPOINT_DOCS_URL,
@@ -5658,17 +5557,17 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
5658
5557
  }
5659
5558
  let varName = configEntryVar ?? await checkEntrypoint(workPath, entrypoint);
5660
5559
  if (!varName) {
5661
- const needsDynamicApp = !!service && ((0, import_build_utils8.isScheduleTriggeredService)(service) || (0, import_build_utils8.isQueueTriggeredService)(service));
5560
+ const needsDynamicApp = !!service && ((0, import_build_utils7.isScheduleTriggeredService)(service) || (0, import_build_utils7.isQueueTriggeredService)(service));
5662
5561
  if (needsDynamicApp) {
5663
5562
  varName = "app";
5664
5563
  }
5665
5564
  }
5666
5565
  if (varName) {
5667
- (0, import_build_utils9.debug)(`Using configured Python entrypoint: ${entrypoint}`);
5566
+ (0, import_build_utils8.debug)(`Using configured Python entrypoint: ${entrypoint}`);
5668
5567
  return { entrypoint: { entrypoint, variableName: varName } };
5669
5568
  } else {
5670
5569
  return {
5671
- error: new import_build_utils8.NowBuildError({
5570
+ error: new import_build_utils7.NowBuildError({
5672
5571
  code: "PYTHON_ENTRYPOINT_NOT_FOUND",
5673
5572
  message: `Could not find a top-level "app", "application", or "handler" in "${entrypoint}".`,
5674
5573
  link: PYTHON_ENTRYPOINT_DOCS_URL,
@@ -5684,36 +5583,18 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
5684
5583
  if (vercelEntry)
5685
5584
  return { entrypoint: vercelEntry };
5686
5585
  let findDiagnostics;
5687
- let searchDirs;
5586
+ let djangoManageBaseDir;
5688
5587
  if (framework === "django") {
5689
5588
  const djangoResult = await detectDjangoPythonEntrypoint(workPath);
5690
5589
  findDiagnostics = djangoResult.findDiagnostics;
5691
- searchDirs = djangoResult.dirs;
5692
5590
  if (djangoResult.detected) {
5693
5591
  if (djangoResult.detected.entrypoint)
5694
5592
  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
- };
5593
+ djangoManageBaseDir = djangoResult.detected.baseDir;
5712
5594
  }
5713
5595
  } else {
5714
5596
  const genericResult = await detectGenericPythonEntrypoint(workPath);
5715
5597
  findDiagnostics = genericResult.findDiagnostics;
5716
- searchDirs = PYTHON_ENTRYPOINT_DIRS;
5717
5598
  if (genericResult.detected)
5718
5599
  return genericResult.detected;
5719
5600
  }
@@ -5721,22 +5602,183 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
5721
5602
  if (pyprojectResult.entrypoint) {
5722
5603
  return { entrypoint: pyprojectResult.entrypoint };
5723
5604
  }
5724
- const candidateSet = new Set(getCandidateEntrypointsInDirs(searchDirs));
5725
- const otherPyFiles = await findOtherPyFiles(
5726
- workPath,
5727
- searchDirs,
5728
- candidateSet
5729
- );
5605
+ const validCandidatesElsewhere = await findEntrypointCandidates(workPath);
5730
5606
  const diagnostics2 = {
5731
5607
  existingWithoutEntrypoint: findDiagnostics.existingWithoutEntrypoint,
5732
- otherPyFiles,
5733
5608
  pyprojectAppScript: pyprojectResult.appScript,
5734
- pyprojectCheckedPaths: pyprojectResult.checkedPaths
5609
+ pyprojectCheckedPaths: pyprojectResult.checkedPaths,
5610
+ validCandidatesElsewhere,
5611
+ djangoManageBaseDir
5612
+ };
5613
+ const error = makeDiagnosticError(framework, diagnostics2);
5614
+ return djangoManageBaseDir !== void 0 ? { baseDir: djangoManageBaseDir, error } : { error };
5615
+ }
5616
+ var detectEntrypoint = async ({
5617
+ workPath,
5618
+ framework
5619
+ }) => {
5620
+ if (!(0, import_build_utils7.isPythonFramework)(framework))
5621
+ return null;
5622
+ const detected = await detectPythonEntrypoint(
5623
+ framework,
5624
+ workPath
5625
+ );
5626
+ if (!detected?.entrypoint)
5627
+ return null;
5628
+ const { entrypoint, variableName } = detected.entrypoint;
5629
+ return {
5630
+ kind: "py-module:attr",
5631
+ entrypoint: `${entrypointToModule(entrypoint)}:${variableName}`
5735
5632
  };
5736
- return { error: makeDiagnosticError(framework, diagnostics2) };
5633
+ };
5634
+
5635
+ // src/crons.ts
5636
+ var DYNAMIC_SCHEDULE = "<dynamic>";
5637
+ var scriptPath = (0, import_path8.join)(__dirname, "..", "templates", "vc_cron_detect.py");
5638
+ var script = import_fs8.default.readFileSync(scriptPath, "utf-8");
5639
+ function buildCronRouteTable(crons) {
5640
+ const table = {};
5641
+ for (const cron of crons) {
5642
+ table[cron.path] = cron.resolvedHandler;
5643
+ }
5644
+ return table;
5645
+ }
5646
+ async function getServiceCrons(opts) {
5647
+ const { service, entrypoint, rawEntrypoint, handlerFunction } = opts;
5648
+ const isScheduledService = !!service && (0, import_build_utils10.isScheduleTriggeredService)(service);
5649
+ if (!isScheduledService || !service.name || typeof service.schedule !== "string" && !Array.isArray(service.schedule)) {
5650
+ return void 0;
5651
+ }
5652
+ const cronEntrypoint = entrypoint || rawEntrypoint;
5653
+ if (!cronEntrypoint) {
5654
+ throw new import_build_utils10.NowBuildError({
5655
+ code: "PYTHON_CRON_NO_ENTRYPOINT",
5656
+ message: "Cron service is missing an entrypoint."
5657
+ });
5658
+ }
5659
+ if (service.schedule === DYNAMIC_SCHEDULE) {
5660
+ return getServiceCronsDynamic({
5661
+ serviceName: service.name,
5662
+ cronEntrypoint,
5663
+ handlerFunction,
5664
+ pythonBin: opts.pythonBin,
5665
+ env: opts.env,
5666
+ workPath: opts.workPath
5667
+ });
5668
+ }
5669
+ const cronPath = (0, import_build_utils10.getInternalServiceCronPath)(
5670
+ service.name,
5671
+ cronEntrypoint,
5672
+ handlerFunction || "cron"
5673
+ );
5674
+ const moduleName = entrypointToModule(cronEntrypoint);
5675
+ const resolvedHandler = handlerFunction ? `${moduleName}:${handlerFunction}` : moduleName;
5676
+ const schedules = Array.isArray(service.schedule) ? service.schedule : [service.schedule];
5677
+ return schedules.map((schedule) => ({
5678
+ path: cronPath,
5679
+ schedule,
5680
+ resolvedHandler
5681
+ }));
5682
+ }
5683
+ async function getServiceCronsDynamic(opts) {
5684
+ const {
5685
+ serviceName,
5686
+ cronEntrypoint,
5687
+ handlerFunction,
5688
+ pythonBin,
5689
+ env,
5690
+ workPath
5691
+ } = opts;
5692
+ if (!handlerFunction) {
5693
+ throw new import_build_utils10.NowBuildError({
5694
+ code: "PYTHON_DYNAMIC_CRON_NO_HANDLER",
5695
+ message: 'Dynamic cron detection requires a "module:object" entrypoint where the object has a get_crons() method.'
5696
+ });
5697
+ }
5698
+ const moduleName = entrypointToModule(cronEntrypoint);
5699
+ const entries = await detectDynamicCrons({
5700
+ pythonBin,
5701
+ env,
5702
+ workPath,
5703
+ moduleName,
5704
+ attrName: handlerFunction
5705
+ });
5706
+ console.log(
5707
+ `Detected ${entries.length} cron entry(s): ${entries.map((e) => `${e.module_function} (${e.schedule})`).join(", ")}`
5708
+ );
5709
+ if (entries.length === 0) {
5710
+ throw new import_build_utils10.NowBuildError({
5711
+ code: "PYTHON_DYNAMIC_CRON_EMPTY",
5712
+ message: `Dynamic cron detection returned no entries. "${moduleName}.${handlerFunction}.get_crons()" returned an empty iterable.`
5713
+ });
5714
+ }
5715
+ return entries.map((entry) => {
5716
+ const cronPath = moduleColonFuncToCronPath(
5717
+ serviceName,
5718
+ entry.module_function
5719
+ );
5720
+ return {
5721
+ path: cronPath,
5722
+ schedule: entry.schedule,
5723
+ resolvedHandler: entry.module_function
5724
+ };
5725
+ });
5726
+ }
5727
+ async function detectDynamicCrons(opts) {
5728
+ const { pythonBin, env, workPath, moduleName, attrName } = opts;
5729
+ let stdout;
5730
+ try {
5731
+ const result = await (0, import_execa4.default)(
5732
+ pythonBin,
5733
+ ["-c", script, moduleName, attrName],
5734
+ { env, cwd: workPath }
5735
+ );
5736
+ stdout = result.stdout;
5737
+ } catch (err) {
5738
+ let detail = err?.stderr || err?.message || String(err);
5739
+ try {
5740
+ const parsed2 = JSON.parse(err?.stdout);
5741
+ if (parsed2.error)
5742
+ detail = parsed2.error;
5743
+ } catch {
5744
+ }
5745
+ throw new import_build_utils10.NowBuildError({
5746
+ code: "PYTHON_DYNAMIC_CRON_DETECTION_FAILED",
5747
+ message: `Failed to detect dynamic cron entries: ${detail}`
5748
+ });
5749
+ }
5750
+ let parsed;
5751
+ try {
5752
+ parsed = JSON.parse(stdout);
5753
+ } catch {
5754
+ throw new import_build_utils10.NowBuildError({
5755
+ code: "PYTHON_DYNAMIC_CRON_DETECTION_FAILED",
5756
+ message: `Dynamic cron detection returned invalid JSON: ${stdout}`
5757
+ });
5758
+ }
5759
+ return parsed.entries || [];
5760
+ }
5761
+ function moduleColonFuncToCronPath(serviceName, moduleFunction) {
5762
+ const colonIdx = moduleFunction.indexOf(":");
5763
+ if (colonIdx === -1) {
5764
+ throw new import_build_utils10.NowBuildError({
5765
+ code: "PYTHON_DYNAMIC_CRON_INVALID_FORMAT",
5766
+ message: `Dynamic cron entry must use "module:function" format, got: "${moduleFunction}"`
5767
+ });
5768
+ }
5769
+ const modulePart = moduleFunction.slice(0, colonIdx);
5770
+ const funcPart = moduleFunction.slice(colonIdx + 1);
5771
+ const entrypointPath = modulePart.replace(/\./g, "/");
5772
+ return (0, import_build_utils10.getInternalServiceCronPath)(serviceName, entrypointPath, funcPart);
5737
5773
  }
5738
5774
 
5739
5775
  // src/start-dev-server.ts
5776
+ var import_child_process2 = require("child_process");
5777
+ var import_fs9 = require("fs");
5778
+ var import_path9 = require("path");
5779
+ var import_build_utils11 = require("@vercel/build-utils");
5780
+ var import_get_port = __toESM(require_get_port());
5781
+ var import_is_port_reachable = __toESM(require_is_port_reachable());
5740
5782
  var import_python_analysis6 = require("@vercel/python-analysis");
5741
5783
  var DEV_SERVER_STARTUP_TIMEOUT = 5 * 6e4;
5742
5784
  function silenceNodeWarnings() {
@@ -6313,7 +6355,7 @@ var startDevServer = async (opts) => {
6313
6355
  });
6314
6356
  }
6315
6357
  const { entrypoint: entry, variableName } = resolved;
6316
- const modulePath = entry.replace(/\.py$/i, "").replace(/[\\/]/g, ".");
6358
+ const modulePath = entrypointToModule(entry);
6317
6359
  let childProcess = null;
6318
6360
  let stdoutLogListener = null;
6319
6361
  let stderrLogListener = null;
@@ -7198,7 +7240,8 @@ var build = async ({
7198
7240
  meta = {},
7199
7241
  config,
7200
7242
  span: parentSpan,
7201
- service
7243
+ service,
7244
+ registerPreDeploy
7202
7245
  }) => {
7203
7246
  let entrypoint = rawEntrypoint === "<detect>" ? void 0 : rawEntrypoint;
7204
7247
  const builderSpan = parentSpan ?? new import_build_utils16.Span({ name: "vc.builder" });
@@ -7442,7 +7485,7 @@ var build = async ({
7442
7485
  await uv.pip({
7443
7486
  venvPath,
7444
7487
  projectDir: (0, import_path13.join)(workPath, entryDirectory),
7445
- args: ["install", runtimeDep]
7488
+ args: ["install", "--link-mode", "copy", runtimeDep]
7446
7489
  });
7447
7490
  if (shouldInstallVercelWorkers) {
7448
7491
  const workersDep = baseEnv.VERCEL_WORKERS_PYTHON || `vercel-workers==${VERCEL_WORKERS_VERSION}`;
@@ -7450,15 +7493,31 @@ var build = async ({
7450
7493
  await uv.pip({
7451
7494
  venvPath,
7452
7495
  projectDir: (0, import_path13.join)(workPath, entryDirectory),
7453
- args: ["install", workersDep]
7496
+ args: ["install", "--link-mode", "copy", workersDep]
7454
7497
  });
7455
7498
  }
7456
7499
  const quirksResult = await runQuirks({ venvPath, pythonEnv, workPath });
7457
7500
  if (quirksResult.buildEnv) {
7458
7501
  Object.assign(pythonEnv, quirksResult.buildEnv);
7459
7502
  }
7503
+ const preDeployCommand = config?.preDeployCommand;
7504
+ if (registerPreDeploy && typeof preDeployCommand === "string") {
7505
+ const capturedEnv = { ...pythonEnv };
7506
+ const capturedCwd = workPath;
7507
+ registerPreDeploy(async () => {
7508
+ await builderSpan.child(import_build_utils16.BUILDER_PRE_DEPLOY_STEP, {
7509
+ preDeployCommand
7510
+ }).trace(async () => {
7511
+ console.log(`Running pre-deploy command: \`${preDeployCommand}\``);
7512
+ await (0, import_build_utils16.execCommand)(preDeployCommand, {
7513
+ env: capturedEnv,
7514
+ cwd: capturedCwd
7515
+ });
7516
+ });
7517
+ });
7518
+ }
7460
7519
  (0, import_build_utils16.debug)("Entrypoint is", entrypoint);
7461
- const moduleName = entrypoint.replace(/\//g, ".").replace(/\.py$/i, "");
7520
+ const moduleName = entrypointToModule(entrypoint);
7462
7521
  if (handlerFunction) {
7463
7522
  const entrypointPath = (0, import_path13.join)(workPath, entrypoint);
7464
7523
  const source = await import_fs13.default.promises.readFile(entrypointPath, "utf-8");
@@ -7697,6 +7756,7 @@ function hasProp(obj, key) {
7697
7756
  0 && (module.exports = {
7698
7757
  build,
7699
7758
  defaultShouldServe,
7759
+ detectEntrypoint,
7700
7760
  diagnostics,
7701
7761
  downloadFilesInWorkPath,
7702
7762
  installRequirement,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/python",
3
- "version": "6.40.0",
3
+ "version": "6.42.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,7 +34,7 @@
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",
37
+ "@vercel/build-utils": "13.25.0",
38
38
  "@vercel/error-utils": "2.1.0",
39
39
  "@vercel/python-runtime": "0.13.2"
40
40
  },