@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.
- package/dist/index.js +421 -318
- 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.
|
|
4375
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
}
|
|
4860
|
-
|
|
4861
|
-
|
|
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
|
-
|
|
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
|
|
5144
|
-
var
|
|
5218
|
+
var import_fs8 = __toESM(require("fs"));
|
|
5219
|
+
var import_path8 = require("path");
|
|
5145
5220
|
var import_execa4 = __toESM(require_execa());
|
|
5146
|
-
var
|
|
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
|
|
5294
|
-
var
|
|
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) =>
|
|
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
|
|
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,
|
|
5308
|
+
const absPath = (0, import_path7.join)(workPath, relPath);
|
|
5374
5309
|
if (!await fileExists(absPath))
|
|
5375
5310
|
return null;
|
|
5376
|
-
const content = await
|
|
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
|
|
5314
|
+
async function findPythonFilesRecursive(dir, rootDir) {
|
|
5380
5315
|
const results = [];
|
|
5381
|
-
|
|
5382
|
-
|
|
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
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
5415
|
+
const absPath = (0, import_path7.join)(workPath, candidate);
|
|
5451
5416
|
if (!await fileExists(absPath))
|
|
5452
5417
|
continue;
|
|
5453
|
-
const content = await
|
|
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,
|
|
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,
|
|
5432
|
+
const managePath = (0, import_path7.join)(workPath, "manage.py");
|
|
5468
5433
|
try {
|
|
5469
|
-
const content = await
|
|
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,
|
|
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
|
|
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
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
5546
|
+
const entrypointExists = await fileExists((0, import_path7.join)(workPath, entrypoint));
|
|
5569
5547
|
if (!entrypointExists) {
|
|
5570
5548
|
return {
|
|
5571
|
-
error: new
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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/
|
|
37
|
+
"@vercel/build-utils": "13.24.0",
|
|
38
38
|
"@vercel/python-runtime": "0.13.2",
|
|
39
|
-
"@vercel/
|
|
39
|
+
"@vercel/error-utils": "2.1.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "node ../../utils/build-builder.mjs",
|