@vercel/python 6.39.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 +421 -318
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -4341,6 +4341,11 @@ var PythonDependencyExternalizer = class {
4341
4341
  this.allVendorFiles = {};
4342
4342
  this.totalBundleSize = 0;
4343
4343
  this.analyzed = false;
4344
+ // Resolved once at the start of analyze(). The venv is immutable
4345
+ // after construction (quirks run before the bundle span) so these
4346
+ // do not change between analyze() and generateBundle().
4347
+ this.sitePackageDirs = null;
4348
+ this.distributions = null;
4344
4349
  this.venvPath = options.venvPath;
4345
4350
  this.vendorDir = options.vendorDir;
4346
4351
  this.workPath = options.workPath;
@@ -4371,8 +4376,17 @@ var PythonDependencyExternalizer = class {
4371
4376
  * Must be called before generateBundle().
4372
4377
  */
4373
4378
  async analyze(files) {
4374
- this.allVendorFiles = await mirrorPackagesIntoVendor({
4375
- venvPath: this.venvPath,
4379
+ this.sitePackageDirs = await getVenvSitePackagesDirs(this.venvPath);
4380
+ this.distributions = /* @__PURE__ */ new Map();
4381
+ for (const dir of this.sitePackageDirs) {
4382
+ try {
4383
+ await import_fs5.default.promises.access(dir);
4384
+ } catch {
4385
+ continue;
4386
+ }
4387
+ this.distributions.set(dir, await (0, import_python_analysis3.scanDistributions)(dir));
4388
+ }
4389
+ this.allVendorFiles = await this.mirrorPackagesIntoVendor({
4376
4390
  vendorDirName: this.vendorDir
4377
4391
  });
4378
4392
  const tempFilesForSizing = { ...files };
@@ -4542,7 +4556,7 @@ To fix this, either:
4542
4556
  wheels available.`
4543
4557
  });
4544
4558
  }
4545
- const packageSizes = await calculatePerPackageSizes(this.venvPath);
4559
+ const packageSizes = await this.calculatePerPackageSizes();
4546
4560
  const alwaysBundled = [
4547
4561
  ...classification.privatePackages,
4548
4562
  "vercel-runtime",
@@ -4550,8 +4564,7 @@ To fix this, either:
4550
4564
  ...this.alwaysBundlePackages,
4551
4565
  ...forceBundledDueToWheels
4552
4566
  ];
4553
- const alwaysBundledFiles = await mirrorPackagesIntoVendor({
4554
- venvPath: this.venvPath,
4567
+ const alwaysBundledFiles = await this.mirrorPackagesIntoVendor({
4555
4568
  vendorDirName: this.vendorDir,
4556
4569
  includePackages: alwaysBundled
4557
4570
  });
@@ -4597,8 +4610,7 @@ To fix this, either:
4597
4610
  );
4598
4611
  const bundledPublic = lambdaKnapsack(publicPackageSizes, remainingCapacity);
4599
4612
  const allBundledPackages = [...alwaysBundled, ...bundledPublic];
4600
- const selectedVendorFiles = await mirrorPackagesIntoVendor({
4601
- venvPath: this.venvPath,
4613
+ const selectedVendorFiles = await this.mirrorPackagesIntoVendor({
4602
4614
  vendorDirName: this.vendorDir,
4603
4615
  includePackages: allBundledPackages
4604
4616
  });
@@ -4747,6 +4759,130 @@ splitting your application.`,
4747
4759
  }
4748
4760
  return incompatible;
4749
4761
  }
4762
+ /**
4763
+ * Mirror packages from site-packages into the _vendor directory.
4764
+ *
4765
+ * When `includePackages` is provided, only distributions whose normalized
4766
+ * name is in the list are included. When omitted, every distribution is
4767
+ * included.
4768
+ *
4769
+ * Reads `this.sitePackageDirs` and `this.distributions` which are
4770
+ * resolved once at the start of `analyze()`.
4771
+ */
4772
+ async mirrorPackagesIntoVendor({
4773
+ vendorDirName,
4774
+ includePackages
4775
+ }) {
4776
+ const vendorFiles = {};
4777
+ if (includePackages && includePackages.length === 0) {
4778
+ return vendorFiles;
4779
+ }
4780
+ const includeSet = includePackages ? new Set(includePackages.map(import_python_analysis3.normalizePackageName)) : null;
4781
+ const pending = [];
4782
+ for (const dir of this.sitePackageDirs) {
4783
+ const dirDistributions = this.distributions.get(dir);
4784
+ if (!dirDistributions)
4785
+ continue;
4786
+ const resolvedDir = (0, import_path6.resolve)(dir);
4787
+ const dirPrefix = resolvedDir + import_path6.sep;
4788
+ for (const [name, dist] of dirDistributions) {
4789
+ if (includeSet && !includeSet.has(name))
4790
+ continue;
4791
+ for (const { path: rawPath, size: recordSize } of dist.files) {
4792
+ const filePath = rawPath.replaceAll("/", import_path6.sep);
4793
+ if (!(0, import_path6.resolve)(resolvedDir, filePath).startsWith(dirPrefix)) {
4794
+ continue;
4795
+ }
4796
+ if (filePath.endsWith(".pyc") || filePath.split(import_path6.sep).includes("__pycache__")) {
4797
+ continue;
4798
+ }
4799
+ const srcFsPath = (0, import_path6.join)(dir, filePath);
4800
+ const bundlePath = (0, import_path6.join)(vendorDirName, filePath).replace(/\\/g, "/");
4801
+ pending.push({
4802
+ bundlePath,
4803
+ srcFsPath,
4804
+ // RECORD sizes are bigint; convert to number for FileFsRef.
4805
+ recordSize: recordSize !== void 0 && recordSize !== null ? Number(recordSize) : void 0
4806
+ });
4807
+ }
4808
+ }
4809
+ }
4810
+ const results = await Promise.all(
4811
+ pending.map(async ({ bundlePath, srcFsPath, recordSize }) => {
4812
+ if (recordSize !== void 0) {
4813
+ try {
4814
+ await import_fs5.default.promises.access(srcFsPath);
4815
+ return { bundlePath, srcFsPath, size: recordSize };
4816
+ } catch {
4817
+ return null;
4818
+ }
4819
+ } else {
4820
+ try {
4821
+ const stats = await import_fs5.default.promises.stat(srcFsPath);
4822
+ return { bundlePath, srcFsPath, size: stats.size };
4823
+ } catch {
4824
+ return null;
4825
+ }
4826
+ }
4827
+ })
4828
+ );
4829
+ for (const result of results) {
4830
+ if (result) {
4831
+ vendorFiles[result.bundlePath] = new import_build_utils5.FileFsRef({
4832
+ fsPath: result.srcFsPath,
4833
+ size: result.size
4834
+ });
4835
+ }
4836
+ }
4837
+ (0, import_build_utils5.debug)(
4838
+ `Mirrored ${Object.keys(vendorFiles).length} files` + (includePackages ? ` from ${includePackages.length} packages` : "")
4839
+ );
4840
+ return vendorFiles;
4841
+ }
4842
+ /**
4843
+ * Calculate the uncompressed size of each installed distribution.
4844
+ *
4845
+ * Returns a map of normalized package name to total size in bytes.
4846
+ * Uses RECORD sizes when available, falling back to stat for files
4847
+ * without a recorded size. All stat calls run in parallel.
4848
+ */
4849
+ async calculatePerPackageSizes() {
4850
+ const sizes = /* @__PURE__ */ new Map();
4851
+ for (const dir of this.sitePackageDirs) {
4852
+ const dirDistributions = this.distributions.get(dir);
4853
+ if (!dirDistributions)
4854
+ continue;
4855
+ const resolvedDir = (0, import_path6.resolve)(dir);
4856
+ const dirPrefix = resolvedDir + import_path6.sep;
4857
+ for (const [name, dist] of dirDistributions) {
4858
+ let knownSize = 0;
4859
+ const statPromises = [];
4860
+ for (const { path: rawPath, size: recordSize } of dist.files) {
4861
+ const filePath = rawPath.replaceAll("/", import_path6.sep);
4862
+ if (!(0, import_path6.resolve)(resolvedDir, filePath).startsWith(dirPrefix)) {
4863
+ continue;
4864
+ }
4865
+ if (filePath.endsWith(".pyc") || filePath.split(import_path6.sep).includes("__pycache__")) {
4866
+ continue;
4867
+ }
4868
+ if (recordSize !== void 0 && recordSize !== null) {
4869
+ knownSize += Number(recordSize);
4870
+ } else {
4871
+ statPromises.push(
4872
+ import_fs5.default.promises.stat((0, import_path6.join)(dir, filePath)).then((stats) => stats.size).catch(() => 0)
4873
+ );
4874
+ }
4875
+ }
4876
+ const statSizes = await Promise.all(statPromises);
4877
+ let totalSize = knownSize;
4878
+ for (const s of statSizes) {
4879
+ totalSize += s;
4880
+ }
4881
+ sizes.set(name, totalSize);
4882
+ }
4883
+ }
4884
+ return sizes;
4885
+ }
4750
4886
  };
4751
4887
  async function getPackagesReachableOnPlatform(lockFile, projectName, pythonMajor, pythonMinor, sysPlatform, platformMachine) {
4752
4888
  if (!projectName)
@@ -4806,66 +4942,35 @@ async function getPackagesReachableOnPlatform(lockFile, projectName, pythonMajor
4806
4942
  }
4807
4943
  return visited;
4808
4944
  }
4809
- async function mirrorPackagesIntoVendor({
4810
- venvPath,
4811
- vendorDirName,
4812
- includePackages
4813
- }) {
4814
- const vendorFiles = {};
4815
- if (includePackages && includePackages.length === 0) {
4816
- return vendorFiles;
4817
- }
4818
- const includeSet = includePackages ? new Set(includePackages.map(import_python_analysis3.normalizePackageName)) : null;
4819
- const sitePackageDirs = await getVenvSitePackagesDirs(venvPath);
4820
- for (const dir of sitePackageDirs) {
4821
- if (!import_fs5.default.existsSync(dir))
4822
- continue;
4823
- const resolvedDir = (0, import_path6.resolve)(dir);
4824
- const dirPrefix = resolvedDir + import_path6.sep;
4825
- const distributions = await (0, import_python_analysis3.scanDistributions)(dir);
4826
- for (const [name, dist] of distributions) {
4827
- if (includeSet && !includeSet.has(name))
4828
- continue;
4829
- for (const { path: rawPath } of dist.files) {
4830
- const filePath = rawPath.replaceAll("/", import_path6.sep);
4831
- if (!(0, import_path6.resolve)(resolvedDir, filePath).startsWith(dirPrefix)) {
4832
- continue;
4833
- }
4834
- if (filePath.endsWith(".pyc") || filePath.split(import_path6.sep).includes("__pycache__")) {
4835
- continue;
4836
- }
4837
- const srcFsPath = (0, import_path6.join)(dir, filePath);
4838
- if (!import_fs5.default.existsSync(srcFsPath)) {
4839
- continue;
4840
- }
4841
- const bundlePath = (0, import_path6.join)(vendorDirName, filePath).replace(/\\/g, "/");
4842
- vendorFiles[bundlePath] = new import_build_utils5.FileFsRef({ fsPath: srcFsPath });
4843
- }
4844
- }
4845
- }
4846
- (0, import_build_utils5.debug)(
4847
- `Mirrored ${Object.keys(vendorFiles).length} files` + (includePackages ? ` from ${includePackages.length} packages` : "")
4848
- );
4849
- return vendorFiles;
4850
- }
4851
4945
  async function calculateBundleSize(files) {
4852
- let totalSize = 0;
4946
+ let knownSize = 0;
4947
+ const statPromises = [];
4853
4948
  for (const filePath of Object.keys(files)) {
4854
4949
  const file = files[filePath];
4855
4950
  if ("fsPath" in file && file.fsPath) {
4856
- try {
4857
- const stats = await import_fs5.default.promises.stat(file.fsPath);
4858
- totalSize += stats.size;
4859
- } catch (err) {
4860
- console.warn(
4861
- `Warning: Failed to stat file ${file.fsPath}, size will not be included in bundle calculation: ${err}`
4951
+ const fsRef = file;
4952
+ if (typeof fsRef.size === "number") {
4953
+ knownSize += fsRef.size;
4954
+ } else {
4955
+ statPromises.push(
4956
+ import_fs5.default.promises.stat(fsRef.fsPath).then((stats) => stats.size).catch((err) => {
4957
+ console.warn(
4958
+ `Warning: Failed to stat file ${fsRef.fsPath}, size will not be included in bundle calculation: ${err}`
4959
+ );
4960
+ return 0;
4961
+ })
4862
4962
  );
4863
4963
  }
4864
4964
  } else if ("data" in file) {
4865
4965
  const data = file.data;
4866
- totalSize += typeof data === "string" ? Buffer.byteLength(data) : data.length;
4966
+ knownSize += typeof data === "string" ? Buffer.byteLength(data) : data.length;
4867
4967
  }
4868
4968
  }
4969
+ const statSizes = await Promise.all(statPromises);
4970
+ let totalSize = knownSize;
4971
+ for (const s of statSizes) {
4972
+ totalSize += s;
4973
+ }
4869
4974
  return totalSize;
4870
4975
  }
4871
4976
  function lambdaKnapsack(packages, capacity) {
@@ -4883,36 +4988,6 @@ function lambdaKnapsack(packages, capacity) {
4883
4988
  }
4884
4989
  return bundled;
4885
4990
  }
4886
- async function calculatePerPackageSizes(venvPath) {
4887
- const sizes = /* @__PURE__ */ new Map();
4888
- const sitePackageDirs = await getVenvSitePackagesDirs(venvPath);
4889
- for (const dir of sitePackageDirs) {
4890
- if (!import_fs5.default.existsSync(dir))
4891
- continue;
4892
- const resolvedDir = (0, import_path6.resolve)(dir);
4893
- const dirPrefix = resolvedDir + import_path6.sep;
4894
- const distributions = await (0, import_python_analysis3.scanDistributions)(dir);
4895
- for (const [name, dist] of distributions) {
4896
- let totalSize = 0;
4897
- for (const { path: rawPath } of dist.files) {
4898
- const filePath = rawPath.replaceAll("/", import_path6.sep);
4899
- if (!(0, import_path6.resolve)(resolvedDir, filePath).startsWith(dirPrefix)) {
4900
- continue;
4901
- }
4902
- if (filePath.endsWith(".pyc") || filePath.split(import_path6.sep).includes("__pycache__")) {
4903
- continue;
4904
- }
4905
- try {
4906
- const stats = await import_fs5.default.promises.stat((0, import_path6.join)(dir, filePath));
4907
- totalSize += stats.size;
4908
- } catch {
4909
- }
4910
- }
4911
- sizes.set(name, totalSize);
4912
- }
4913
- }
4914
- return sizes;
4915
- }
4916
4991
 
4917
4992
  // src/diagnostics.ts
4918
4993
  var import_fs6 = __toESM(require("fs"));
@@ -5140,161 +5215,17 @@ async function generateProjectManifest({
5140
5215
  var diagnostics = (0, import_build_utils6.createDiagnostics)("python");
5141
5216
 
5142
5217
  // src/crons.ts
5143
- var import_fs7 = __toESM(require("fs"));
5144
- var import_path7 = require("path");
5218
+ var import_fs8 = __toESM(require("fs"));
5219
+ var import_path8 = require("path");
5145
5220
  var import_execa4 = __toESM(require_execa());
5146
- var import_build_utils7 = require("@vercel/build-utils");
5147
- var DYNAMIC_SCHEDULE = "<dynamic>";
5148
- function entrypointToModule(entrypoint) {
5149
- return entrypoint.replace(/\\/g, "/").replace(/\.py$/i, "").replace(/\//g, ".");
5150
- }
5151
- var scriptPath = (0, import_path7.join)(__dirname, "..", "templates", "vc_cron_detect.py");
5152
- var script = import_fs7.default.readFileSync(scriptPath, "utf-8");
5153
- function buildCronRouteTable(crons) {
5154
- const table = {};
5155
- for (const cron of crons) {
5156
- table[cron.path] = cron.resolvedHandler;
5157
- }
5158
- return table;
5159
- }
5160
- async function getServiceCrons(opts) {
5161
- const { service, entrypoint, rawEntrypoint, handlerFunction } = opts;
5162
- const isScheduledService = !!service && (0, import_build_utils7.isScheduleTriggeredService)(service);
5163
- if (!isScheduledService || !service.name || typeof service.schedule !== "string") {
5164
- return void 0;
5165
- }
5166
- const cronEntrypoint = entrypoint || rawEntrypoint;
5167
- if (!cronEntrypoint) {
5168
- throw new import_build_utils7.NowBuildError({
5169
- code: "PYTHON_CRON_NO_ENTRYPOINT",
5170
- message: "Cron service is missing an entrypoint."
5171
- });
5172
- }
5173
- if (service.schedule === DYNAMIC_SCHEDULE) {
5174
- return getServiceCronsDynamic({
5175
- serviceName: service.name,
5176
- cronEntrypoint,
5177
- handlerFunction,
5178
- pythonBin: opts.pythonBin,
5179
- env: opts.env,
5180
- workPath: opts.workPath
5181
- });
5182
- }
5183
- const cronPath = (0, import_build_utils7.getInternalServiceCronPath)(
5184
- service.name,
5185
- cronEntrypoint,
5186
- handlerFunction || "cron"
5187
- );
5188
- const moduleName = entrypointToModule(cronEntrypoint);
5189
- const resolvedHandler = handlerFunction ? `${moduleName}:${handlerFunction}` : moduleName;
5190
- return [{ path: cronPath, schedule: service.schedule, resolvedHandler }];
5191
- }
5192
- async function getServiceCronsDynamic(opts) {
5193
- const {
5194
- serviceName,
5195
- cronEntrypoint,
5196
- handlerFunction,
5197
- pythonBin,
5198
- env,
5199
- workPath
5200
- } = opts;
5201
- if (!handlerFunction) {
5202
- throw new import_build_utils7.NowBuildError({
5203
- code: "PYTHON_DYNAMIC_CRON_NO_HANDLER",
5204
- message: 'Dynamic cron detection requires a "module:object" entrypoint where the object has a get_crons() method.'
5205
- });
5206
- }
5207
- const moduleName = entrypointToModule(cronEntrypoint);
5208
- const entries = await detectDynamicCrons({
5209
- pythonBin,
5210
- env,
5211
- workPath,
5212
- moduleName,
5213
- attrName: handlerFunction
5214
- });
5215
- console.log(
5216
- `Detected ${entries.length} cron entry(s): ${entries.map((e) => `${e.module_function} (${e.schedule})`).join(", ")}`
5217
- );
5218
- if (entries.length === 0) {
5219
- throw new import_build_utils7.NowBuildError({
5220
- code: "PYTHON_DYNAMIC_CRON_EMPTY",
5221
- message: `Dynamic cron detection returned no entries. "${moduleName}.${handlerFunction}.get_crons()" returned an empty iterable.`
5222
- });
5223
- }
5224
- return entries.map((entry) => {
5225
- const cronPath = moduleColonFuncToCronPath(
5226
- serviceName,
5227
- entry.module_function
5228
- );
5229
- return {
5230
- path: cronPath,
5231
- schedule: entry.schedule,
5232
- resolvedHandler: entry.module_function
5233
- };
5234
- });
5235
- }
5236
- async function detectDynamicCrons(opts) {
5237
- const { pythonBin, env, workPath, moduleName, attrName } = opts;
5238
- let stdout;
5239
- try {
5240
- const result = await (0, import_execa4.default)(
5241
- pythonBin,
5242
- ["-c", script, moduleName, attrName],
5243
- { env, cwd: workPath }
5244
- );
5245
- stdout = result.stdout;
5246
- } catch (err) {
5247
- let detail = err?.stderr || err?.message || String(err);
5248
- try {
5249
- const parsed2 = JSON.parse(err?.stdout);
5250
- if (parsed2.error)
5251
- detail = parsed2.error;
5252
- } catch {
5253
- }
5254
- throw new import_build_utils7.NowBuildError({
5255
- code: "PYTHON_DYNAMIC_CRON_DETECTION_FAILED",
5256
- message: `Failed to detect dynamic cron entries: ${detail}`
5257
- });
5258
- }
5259
- let parsed;
5260
- try {
5261
- parsed = JSON.parse(stdout);
5262
- } catch {
5263
- throw new import_build_utils7.NowBuildError({
5264
- code: "PYTHON_DYNAMIC_CRON_DETECTION_FAILED",
5265
- message: `Dynamic cron detection returned invalid JSON: ${stdout}`
5266
- });
5267
- }
5268
- return parsed.entries || [];
5269
- }
5270
- function moduleColonFuncToCronPath(serviceName, moduleFunction) {
5271
- const colonIdx = moduleFunction.indexOf(":");
5272
- if (colonIdx === -1) {
5273
- throw new import_build_utils7.NowBuildError({
5274
- code: "PYTHON_DYNAMIC_CRON_INVALID_FORMAT",
5275
- message: `Dynamic cron entry must use "module:function" format, got: "${moduleFunction}"`
5276
- });
5277
- }
5278
- const modulePart = moduleFunction.slice(0, colonIdx);
5279
- const funcPart = moduleFunction.slice(colonIdx + 1);
5280
- const entrypointPath = modulePart.replace(/\./g, "/");
5281
- return (0, import_build_utils7.getInternalServiceCronPath)(serviceName, entrypointPath, funcPart);
5282
- }
5283
-
5284
- // src/start-dev-server.ts
5285
- var import_child_process2 = require("child_process");
5286
- var import_fs9 = require("fs");
5287
- var import_path9 = require("path");
5288
- var import_build_utils11 = require("@vercel/build-utils");
5289
- var import_get_port = __toESM(require_get_port());
5290
- var import_is_port_reachable = __toESM(require_is_port_reachable());
5221
+ var import_build_utils10 = require("@vercel/build-utils");
5291
5222
 
5292
5223
  // src/entrypoint.ts
5293
- var import_fs8 = __toESM(require("fs"));
5294
- 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");
5295
5227
  var import_build_utils8 = require("@vercel/build-utils");
5296
5228
  var import_build_utils9 = require("@vercel/build-utils");
5297
- var import_build_utils10 = require("@vercel/build-utils");
5298
5229
  var import_python_analysis5 = require("@vercel/python-analysis");
5299
5230
  var PYTHON_ENTRYPOINT_FILENAMES = [
5300
5231
  "app",
@@ -5309,16 +5240,17 @@ var PYTHON_CANDIDATE_ENTRYPOINTS = getCandidateEntrypointsInDirs(
5309
5240
  PYTHON_ENTRYPOINT_DIRS
5310
5241
  );
5311
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"]);
5312
5244
  function getCandidateEntrypointsInDirs(dirs) {
5313
5245
  return dirs.flatMap(
5314
5246
  (dir) => PYTHON_ENTRYPOINT_FILENAMES.map(
5315
- (filename) => import_path8.posix.join(dir, `${filename}.py`)
5247
+ (filename) => import_path7.posix.join(dir, `${filename}.py`)
5316
5248
  )
5317
5249
  );
5318
5250
  }
5319
5251
  async function fileExists(filePath) {
5320
5252
  try {
5321
- const stat = await import_fs8.default.promises.stat(filePath);
5253
+ const stat = await import_fs7.default.promises.stat(filePath);
5322
5254
  return stat.isFile();
5323
5255
  } catch {
5324
5256
  return false;
@@ -5369,34 +5301,67 @@ function getMissingExportMessage(framework, entrypoints) {
5369
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.`;
5370
5302
  }
5371
5303
  }
5304
+ function entrypointToModule(entrypoint) {
5305
+ return entrypoint.replace(/\\/g, "/").replace(/\.py$/i, "").replace(/\//g, ".");
5306
+ }
5372
5307
  async function checkEntrypoint(workPath, relPath) {
5373
- const absPath = (0, import_path8.join)(workPath, relPath);
5308
+ const absPath = (0, import_path7.join)(workPath, relPath);
5374
5309
  if (!await fileExists(absPath))
5375
5310
  return null;
5376
- const content = await import_fs8.default.promises.readFile(absPath, "utf-8");
5311
+ const content = await import_fs7.default.promises.readFile(absPath, "utf-8");
5377
5312
  return (0, import_python_analysis5.findAppOrHandler)(content);
5378
5313
  }
5379
- async function findOtherPyFiles(workPath, dirs, candidateSet) {
5314
+ async function findPythonFilesRecursive(dir, rootDir) {
5380
5315
  const results = [];
5381
- for (const dir of dirs) {
5382
- 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) {
5383
5338
  try {
5384
- const entries = await import_fs8.default.promises.readdir(dirPath);
5385
- for (const entry of entries) {
5386
- if (!entry.endsWith(".py"))
5387
- continue;
5388
- const relPath = dir ? import_path8.posix.join(dir, entry) : entry;
5389
- if (candidateSet.has(relPath))
5390
- continue;
5391
- const stat = await import_fs8.default.promises.stat((0, import_path8.join)(dirPath, entry));
5392
- if (stat.isFile()) {
5393
- results.push(relPath);
5394
- }
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 });
5395
5346
  }
5396
5347
  } catch {
5397
5348
  }
5398
5349
  }
5399
- 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}"`;
5400
5365
  }
5401
5366
  async function resolveModuleAttrEntrypoint(workPath, value) {
5402
5367
  const match = value.match(/([A-Za-z_][\w.]*)\s*:\s*([A-Za-z_][\w]*)/);
@@ -5407,14 +5372,14 @@ async function resolveModuleAttrEntrypoint(workPath, value) {
5407
5372
  const relPath = modulePath.replace(/\./g, "/");
5408
5373
  const candidates = [`${relPath}.py`, `${relPath}/__init__.py`];
5409
5374
  for (const candidate of candidates) {
5410
- if (await fileExists((0, import_path8.join)(workPath, candidate))) {
5375
+ if (await fileExists((0, import_path7.join)(workPath, candidate))) {
5411
5376
  return { entrypoint: candidate, variableName };
5412
5377
  }
5413
5378
  }
5414
5379
  return null;
5415
5380
  }
5416
5381
  async function getVercelToolsEntrypoint(workPath) {
5417
- 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"));
5418
5383
  if (!pyprojectData)
5419
5384
  return null;
5420
5385
  const vercelEntrypoint = pyprojectData.tool?.vercel?.entrypoint;
@@ -5423,7 +5388,7 @@ async function getVercelToolsEntrypoint(workPath) {
5423
5388
  return resolveModuleAttrEntrypoint(workPath, vercelEntrypoint);
5424
5389
  }
5425
5390
  async function getPyprojectEntrypointWithDiagnostics(workPath) {
5426
- 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"));
5427
5392
  if (!pyprojectData)
5428
5393
  return { entrypoint: null };
5429
5394
  const scripts = pyprojectData.project?.scripts;
@@ -5438,7 +5403,7 @@ async function getPyprojectEntrypointWithDiagnostics(workPath) {
5438
5403
  const relPath = modulePath.replace(/\./g, "/");
5439
5404
  const candidates = [`${relPath}.py`, `${relPath}/__init__.py`];
5440
5405
  for (const candidate of candidates) {
5441
- if (await fileExists((0, import_path8.join)(workPath, candidate))) {
5406
+ if (await fileExists((0, import_path7.join)(workPath, candidate))) {
5442
5407
  return { entrypoint: { entrypoint: candidate, variableName } };
5443
5408
  }
5444
5409
  }
@@ -5447,13 +5412,13 @@ async function getPyprojectEntrypointWithDiagnostics(workPath) {
5447
5412
  async function findValidEntrypoint(workPath, candidates) {
5448
5413
  const existingWithoutEntrypoint = [];
5449
5414
  for (const candidate of candidates) {
5450
- const absPath = (0, import_path8.join)(workPath, candidate);
5415
+ const absPath = (0, import_path7.join)(workPath, candidate);
5451
5416
  if (!await fileExists(absPath))
5452
5417
  continue;
5453
- const content = await import_fs8.default.promises.readFile(absPath, "utf-8");
5418
+ const content = await import_fs7.default.promises.readFile(absPath, "utf-8");
5454
5419
  const varName = await (0, import_python_analysis5.findAppOrHandler)(content);
5455
5420
  if (varName) {
5456
- (0, import_build_utils9.debug)(`Detected Python entrypoint: ${candidate} (variable: ${varName})`);
5421
+ (0, import_build_utils8.debug)(`Detected Python entrypoint: ${candidate} (variable: ${varName})`);
5457
5422
  return {
5458
5423
  entrypoint: { entrypoint: candidate, variableName: varName },
5459
5424
  existingWithoutEntrypoint
@@ -5464,12 +5429,12 @@ async function findValidEntrypoint(workPath, candidates) {
5464
5429
  return { entrypoint: null, existingWithoutEntrypoint };
5465
5430
  }
5466
5431
  async function checkDjangoManage(workPath) {
5467
- const managePath = (0, import_path8.join)(workPath, "manage.py");
5432
+ const managePath = (0, import_path7.join)(workPath, "manage.py");
5468
5433
  try {
5469
- const content = await import_fs8.default.promises.readFile(managePath, "utf-8");
5434
+ const content = await import_fs7.default.promises.readFile(managePath, "utf-8");
5470
5435
  if (!content.includes("DJANGO_SETTINGS_MODULE"))
5471
5436
  return false;
5472
- (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}`);
5473
5438
  return true;
5474
5439
  } catch {
5475
5440
  return false;
@@ -5477,7 +5442,7 @@ async function checkDjangoManage(workPath) {
5477
5442
  }
5478
5443
  async function getSubdirectories(workPath) {
5479
5444
  try {
5480
- const entries = await import_fs8.default.promises.readdir(workPath, {
5445
+ const entries = await import_fs7.default.promises.readdir(workPath, {
5481
5446
  withFileTypes: true
5482
5447
  });
5483
5448
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
@@ -5490,23 +5455,39 @@ function makeDiagnosticError(framework, diagnostics2) {
5490
5455
  const link = getFrameworkEntrypointDocsUrl(framework);
5491
5456
  const action = "Learn More";
5492
5457
  const displayName = getFrameworkDisplayName(framework);
5458
+ const elsewhere = diagnostics2.validCandidatesElsewhere ?? [];
5459
+ const suggestion = elsewhere.length > 0 ? renderCandidateSuggestion(elsewhere) : null;
5493
5460
  let message;
5494
5461
  if (diagnostics2.existingWithoutEntrypoint.length > 0) {
5495
5462
  message = getMissingExportMessage(
5496
5463
  framework,
5497
5464
  diagnostics2.existingWithoutEntrypoint
5498
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}`;
5499
5477
  } else if (diagnostics2.pyprojectAppScript && diagnostics2.pyprojectCheckedPaths?.length) {
5500
5478
  const checked = diagnostics2.pyprojectCheckedPaths.join(" or ");
5501
5479
  message = `pyproject.toml [project.scripts] defines app = "${diagnostics2.pyprojectAppScript}" but ${checked} was not found.`;
5502
- } else if (diagnostics2.otherPyFiles.length > 0) {
5503
- const fileList = diagnostics2.otherPyFiles.slice(0, 5).join(", ");
5504
- 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}`;
5505
5486
  } else {
5506
5487
  const searchedList = PYTHON_CANDIDATE_ENTRYPOINTS.join(", ");
5507
5488
  message = `No ${displayName} entrypoint found. Set "tool.vercel.entrypoint" in pyproject.toml or define an entrypoint in one of: ${searchedList}.`;
5508
5489
  }
5509
- return new import_build_utils8.NowBuildError({ code, message, link, action });
5490
+ return new import_build_utils7.NowBuildError({ code, message, link, action });
5510
5491
  }
5511
5492
  async function detectGenericPythonEntrypoint(workPath) {
5512
5493
  try {
@@ -5519,7 +5500,7 @@ async function detectGenericPythonEntrypoint(workPath) {
5519
5500
  findDiagnostics: result
5520
5501
  };
5521
5502
  } catch {
5522
- (0, import_build_utils9.debug)("Failed to discover Python entrypoint");
5503
+ (0, import_build_utils8.debug)("Failed to discover Python entrypoint");
5523
5504
  return {
5524
5505
  detected: null,
5525
5506
  findDiagnostics: { entrypoint: null, existingWithoutEntrypoint: [] }
@@ -5532,20 +5513,18 @@ async function detectDjangoPythonEntrypoint(workPath) {
5532
5513
  findDiagnostics: {
5533
5514
  entrypoint: null,
5534
5515
  existingWithoutEntrypoint: []
5535
- },
5536
- dirs: PYTHON_ENTRYPOINT_DIRS
5516
+ }
5537
5517
  };
5538
5518
  try {
5539
5519
  const subdirs = await getSubdirectories(workPath);
5540
5520
  const rootDirs = ["", ...subdirs];
5541
5521
  for (const rootDir of rootDirs) {
5542
- const currPath = (0, import_path8.join)(workPath, rootDir);
5522
+ const currPath = (0, import_path7.join)(workPath, rootDir);
5543
5523
  const isDjango = await checkDjangoManage(currPath);
5544
5524
  if (isDjango) {
5545
5525
  return {
5546
5526
  detected: { baseDir: rootDir },
5547
- findDiagnostics: { entrypoint: null, existingWithoutEntrypoint: [] },
5548
- dirs: rootDirs
5527
+ findDiagnostics: { entrypoint: null, existingWithoutEntrypoint: [] }
5549
5528
  };
5550
5529
  }
5551
5530
  }
@@ -5553,11 +5532,10 @@ async function detectDjangoPythonEntrypoint(workPath) {
5553
5532
  const result = await findValidEntrypoint(workPath, candidates);
5554
5533
  return {
5555
5534
  detected: result.entrypoint ? { entrypoint: result.entrypoint } : null,
5556
- findDiagnostics: result,
5557
- dirs: rootDirs
5535
+ findDiagnostics: result
5558
5536
  };
5559
5537
  } catch {
5560
- (0, import_build_utils9.debug)("Failed to discover Django Python entrypoint");
5538
+ (0, import_build_utils8.debug)("Failed to discover Django Python entrypoint");
5561
5539
  return emptyResult;
5562
5540
  }
5563
5541
  }
@@ -5565,10 +5543,10 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
5565
5543
  if (configuredEntrypoint) {
5566
5544
  const { filePath: configEntryFile, varName: configEntryVar } = configuredEntrypoint;
5567
5545
  const entrypoint = configEntryFile.endsWith(".py") ? configEntryFile : `${configEntryFile}.py`;
5568
- const entrypointExists = await fileExists((0, import_path8.join)(workPath, entrypoint));
5546
+ const entrypointExists = await fileExists((0, import_path7.join)(workPath, entrypoint));
5569
5547
  if (!entrypointExists) {
5570
5548
  return {
5571
- error: new import_build_utils8.NowBuildError({
5549
+ error: new import_build_utils7.NowBuildError({
5572
5550
  code: "PYTHON_ENTRYPOINT_NOT_FOUND",
5573
5551
  message: `Configured Python entrypoint "${entrypoint}" was not found.`,
5574
5552
  link: PYTHON_ENTRYPOINT_DOCS_URL,
@@ -5578,17 +5556,17 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
5578
5556
  }
5579
5557
  let varName = configEntryVar ?? await checkEntrypoint(workPath, entrypoint);
5580
5558
  if (!varName) {
5581
- 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));
5582
5560
  if (needsDynamicApp) {
5583
5561
  varName = "app";
5584
5562
  }
5585
5563
  }
5586
5564
  if (varName) {
5587
- (0, import_build_utils9.debug)(`Using configured Python entrypoint: ${entrypoint}`);
5565
+ (0, import_build_utils8.debug)(`Using configured Python entrypoint: ${entrypoint}`);
5588
5566
  return { entrypoint: { entrypoint, variableName: varName } };
5589
5567
  } else {
5590
5568
  return {
5591
- error: new import_build_utils8.NowBuildError({
5569
+ error: new import_build_utils7.NowBuildError({
5592
5570
  code: "PYTHON_ENTRYPOINT_NOT_FOUND",
5593
5571
  message: `Could not find a top-level "app", "application", or "handler" in "${entrypoint}".`,
5594
5572
  link: PYTHON_ENTRYPOINT_DOCS_URL,
@@ -5604,36 +5582,18 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
5604
5582
  if (vercelEntry)
5605
5583
  return { entrypoint: vercelEntry };
5606
5584
  let findDiagnostics;
5607
- let searchDirs;
5585
+ let djangoManageBaseDir;
5608
5586
  if (framework === "django") {
5609
5587
  const djangoResult = await detectDjangoPythonEntrypoint(workPath);
5610
5588
  findDiagnostics = djangoResult.findDiagnostics;
5611
- searchDirs = djangoResult.dirs;
5612
5589
  if (djangoResult.detected) {
5613
5590
  if (djangoResult.detected.entrypoint)
5614
5591
  return djangoResult.detected;
5615
- const candidateSet2 = new Set(getCandidateEntrypointsInDirs(searchDirs));
5616
- const otherPyFiles2 = await findOtherPyFiles(
5617
- workPath,
5618
- searchDirs,
5619
- candidateSet2
5620
- );
5621
- const pyprojectResult2 = await getPyprojectEntrypointWithDiagnostics(workPath);
5622
- const diagnostics3 = {
5623
- existingWithoutEntrypoint: findDiagnostics.existingWithoutEntrypoint,
5624
- otherPyFiles: otherPyFiles2,
5625
- pyprojectAppScript: pyprojectResult2.appScript,
5626
- pyprojectCheckedPaths: pyprojectResult2.checkedPaths
5627
- };
5628
- return {
5629
- baseDir: djangoResult.detected.baseDir,
5630
- error: makeDiagnosticError("django", diagnostics3)
5631
- };
5592
+ djangoManageBaseDir = djangoResult.detected.baseDir;
5632
5593
  }
5633
5594
  } else {
5634
5595
  const genericResult = await detectGenericPythonEntrypoint(workPath);
5635
5596
  findDiagnostics = genericResult.findDiagnostics;
5636
- searchDirs = PYTHON_ENTRYPOINT_DIRS;
5637
5597
  if (genericResult.detected)
5638
5598
  return genericResult.detected;
5639
5599
  }
@@ -5641,22 +5601,165 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
5641
5601
  if (pyprojectResult.entrypoint) {
5642
5602
  return { entrypoint: pyprojectResult.entrypoint };
5643
5603
  }
5644
- const candidateSet = new Set(getCandidateEntrypointsInDirs(searchDirs));
5645
- const otherPyFiles = await findOtherPyFiles(
5646
- workPath,
5647
- searchDirs,
5648
- candidateSet
5649
- );
5604
+ const validCandidatesElsewhere = await findEntrypointCandidates(workPath);
5650
5605
  const diagnostics2 = {
5651
5606
  existingWithoutEntrypoint: findDiagnostics.existingWithoutEntrypoint,
5652
- otherPyFiles,
5653
5607
  pyprojectAppScript: pyprojectResult.appScript,
5654
- pyprojectCheckedPaths: pyprojectResult.checkedPaths
5608
+ pyprojectCheckedPaths: pyprojectResult.checkedPaths,
5609
+ validCandidatesElsewhere,
5610
+ djangoManageBaseDir
5655
5611
  };
5656
- 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);
5657
5754
  }
5658
5755
 
5659
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());
5660
5763
  var import_python_analysis6 = require("@vercel/python-analysis");
5661
5764
  var DEV_SERVER_STARTUP_TIMEOUT = 5 * 6e4;
5662
5765
  function silenceNodeWarnings() {
@@ -6233,7 +6336,7 @@ var startDevServer = async (opts) => {
6233
6336
  });
6234
6337
  }
6235
6338
  const { entrypoint: entry, variableName } = resolved;
6236
- const modulePath = entry.replace(/\.py$/i, "").replace(/[\\/]/g, ".");
6339
+ const modulePath = entrypointToModule(entry);
6237
6340
  let childProcess = null;
6238
6341
  let stdoutLogListener = null;
6239
6342
  let stderrLogListener = null;
@@ -7362,7 +7465,7 @@ var build = async ({
7362
7465
  await uv.pip({
7363
7466
  venvPath,
7364
7467
  projectDir: (0, import_path13.join)(workPath, entryDirectory),
7365
- args: ["install", runtimeDep]
7468
+ args: ["install", "--link-mode", "copy", runtimeDep]
7366
7469
  });
7367
7470
  if (shouldInstallVercelWorkers) {
7368
7471
  const workersDep = baseEnv.VERCEL_WORKERS_PYTHON || `vercel-workers==${VERCEL_WORKERS_VERSION}`;
@@ -7370,7 +7473,7 @@ var build = async ({
7370
7473
  await uv.pip({
7371
7474
  venvPath,
7372
7475
  projectDir: (0, import_path13.join)(workPath, entryDirectory),
7373
- args: ["install", workersDep]
7476
+ args: ["install", "--link-mode", "copy", workersDep]
7374
7477
  });
7375
7478
  }
7376
7479
  const quirksResult = await runQuirks({ venvPath, pythonEnv, workPath });
@@ -7378,7 +7481,7 @@ var build = async ({
7378
7481
  Object.assign(pythonEnv, quirksResult.buildEnv);
7379
7482
  }
7380
7483
  (0, import_build_utils16.debug)("Entrypoint is", entrypoint);
7381
- const moduleName = entrypoint.replace(/\//g, ".").replace(/\.py$/i, "");
7484
+ const moduleName = entrypointToModule(entrypoint);
7382
7485
  if (handlerFunction) {
7383
7486
  const entrypointPath = (0, import_path13.join)(workPath, entrypoint);
7384
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.39.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/error-utils": "2.1.0",
37
+ "@vercel/build-utils": "13.24.0",
38
38
  "@vercel/python-runtime": "0.13.2",
39
- "@vercel/build-utils": "13.22.0"
39
+ "@vercel/error-utils": "2.1.0"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "node ../../utils/build-builder.mjs",