@vercel/build-utils 13.2.2 → 13.2.4

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.
@@ -0,0 +1,22 @@
1
+ import FileFsRef from './file-fs-ref';
2
+ /**
3
+ * Run a Python script that only uses the standard library.
4
+ */
5
+ export declare function runStdlibPyScript(options: {
6
+ scriptName: string;
7
+ pythonPath?: string;
8
+ args?: string[];
9
+ cwd?: string;
10
+ }): Promise<{
11
+ exitCode: number;
12
+ stdout: string;
13
+ stderr: string;
14
+ }>;
15
+ /**
16
+ * Check if a Python file is a valid entrypoint by detecting:
17
+ * - A top-level 'app' callable (Flask, FastAPI, Sanic, WSGI/ASGI, etc.)
18
+ * - A top-level 'handler' class (BaseHTTPRequestHandler subclass)
19
+ */
20
+ export declare function isPythonEntrypoint(file: FileFsRef | {
21
+ fsPath?: string;
22
+ }): Promise<boolean>;
package/dist/python.js ADDED
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var python_exports = {};
30
+ __export(python_exports, {
31
+ isPythonEntrypoint: () => isPythonEntrypoint,
32
+ runStdlibPyScript: () => runStdlibPyScript
33
+ });
34
+ module.exports = __toCommonJS(python_exports);
35
+ var import_fs = __toESM(require("fs"));
36
+ var import_path = require("path");
37
+ var import_execa = __toESM(require("execa"));
38
+ var import_debug = __toESM(require("./debug"));
39
+ const isWin = process.platform === "win32";
40
+ async function runStdlibPyScript(options) {
41
+ const { scriptName, pythonPath, args = [], cwd } = options;
42
+ const scriptPath = (0, import_path.join)(__dirname, "..", "lib", "python", `${scriptName}.py`);
43
+ if (!import_fs.default.existsSync(scriptPath)) {
44
+ throw new Error(`Python script not found: ${scriptPath}`);
45
+ }
46
+ const pythonCmd = pythonPath ?? (isWin ? "python" : "python3");
47
+ (0, import_debug.default)(
48
+ `Running stdlib Python script: ${pythonCmd} ${scriptPath} ${args.join(" ")}`
49
+ );
50
+ try {
51
+ const result = await (0, import_execa.default)(pythonCmd, [scriptPath, ...args], { cwd });
52
+ return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
53
+ } catch (err) {
54
+ const execaErr = err;
55
+ return {
56
+ exitCode: execaErr.exitCode ?? 1,
57
+ stdout: execaErr.stdout ?? "",
58
+ stderr: execaErr.stderr ?? ""
59
+ };
60
+ }
61
+ }
62
+ async function isPythonEntrypoint(file) {
63
+ try {
64
+ const fsPath = file.fsPath;
65
+ if (!fsPath)
66
+ return false;
67
+ const content = await import_fs.default.promises.readFile(fsPath, "utf-8");
68
+ if (!content.includes("app") && !content.includes("handler") && !content.includes("Handler")) {
69
+ return false;
70
+ }
71
+ const result = await runStdlibPyScript({
72
+ scriptName: "ast_parser",
73
+ args: [fsPath]
74
+ });
75
+ return result.exitCode === 0;
76
+ } catch (err) {
77
+ (0, import_debug.default)(`Failed to check Python entrypoint: ${err}`);
78
+ return false;
79
+ }
80
+ }
81
+ // Annotate the CommonJS export names for ESM import in node:
82
+ 0 && (module.exports = {
83
+ isPythonEntrypoint,
84
+ runStdlibPyScript
85
+ });
@@ -0,0 +1,72 @@
1
+ import sys
2
+ import ast
3
+
4
+
5
+ def contains_app_or_handler(file_path: str) -> bool:
6
+ """
7
+ Check if a Python file contains or exports:
8
+ - A top-level 'app' callable (e.g., Flask, FastAPI, Sanic apps)
9
+ - A top-level 'handler' class (e.g., BaseHTTPRequestHandler subclass)
10
+ """
11
+ with open(file_path, "r") as file:
12
+ code = file.read()
13
+
14
+ try:
15
+ tree = ast.parse(code)
16
+ except SyntaxError:
17
+ return False
18
+
19
+ for node in ast.iter_child_nodes(tree):
20
+ # Check for top-level assignment to 'app'
21
+ # e.g., app = Sanic() or app = Flask(__name__) or app = create_app()
22
+ if isinstance(node, ast.Assign):
23
+ for target in node.targets:
24
+ if isinstance(target, ast.Name) and target.id == "app":
25
+ return True
26
+
27
+ # Check for annotated assignment to 'app'
28
+ # e.g., app: Sanic = Sanic()
29
+ if isinstance(node, ast.AnnAssign):
30
+ if isinstance(node.target, ast.Name) and node.target.id == "app":
31
+ return True
32
+
33
+ # Check for function named 'app'
34
+ # e.g., def app(environ, start_response): ...
35
+ if isinstance(node, ast.FunctionDef) and node.name == "app":
36
+ return True
37
+
38
+ # Check for async function named 'app'
39
+ # e.g., async def app(scope, receive, send): ...
40
+ if isinstance(node, ast.AsyncFunctionDef) and node.name == "app":
41
+ return True
42
+
43
+ # Check for import of 'app'
44
+ # e.g., from server import app
45
+ # e.g., from server import application as app
46
+ if isinstance(node, ast.ImportFrom):
47
+ for alias in node.names:
48
+ # alias.asname is the 'as' name, alias.name is the original name
49
+ # If aliased, check asname; otherwise check the original name
50
+ imported_as = alias.asname if alias.asname else alias.name
51
+ if imported_as == "app":
52
+ return True
53
+
54
+ # Check for top-level class named 'handler'
55
+ # e.g., class handler(BaseHTTPRequestHandler):
56
+ if isinstance(node, ast.ClassDef) and node.name.lower() == "handler":
57
+ return True
58
+
59
+ return False
60
+
61
+
62
+ if __name__ == "__main__":
63
+ if len(sys.argv) != 2:
64
+ print("Usage: python ast_parser.py <file_path>")
65
+ sys.exit(1)
66
+
67
+ file_path = sys.argv[1]
68
+ result = contains_app_or_handler(file_path)
69
+
70
+ # Exit with 0 if found, 1 if not found
71
+ sys.exit(0 if result else 1)
72
+
@@ -0,0 +1,72 @@
1
+ import unittest
2
+ import tempfile
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ sys.path.insert(0, str(Path(__file__).parent.parent))
8
+
9
+ from ast_parser import contains_app_or_handler
10
+
11
+
12
+ class TestContainsAppOrHandler(unittest.TestCase):
13
+ def _check(self, code: str) -> bool:
14
+ """Helper to test code snippets without needing fixture files."""
15
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
16
+ f.write(code)
17
+ f.flush()
18
+ try:
19
+ return contains_app_or_handler(f.name)
20
+ finally:
21
+ os.unlink(f.name)
22
+
23
+ def test_flask_app(self):
24
+ self.assertTrue(self._check("from flask import Flask\napp = Flask(__name__)"))
25
+
26
+ def test_fastapi_app(self):
27
+ self.assertTrue(self._check("from fastapi import FastAPI\napp = FastAPI()"))
28
+
29
+ def test_sanic_app(self):
30
+ self.assertTrue(self._check("from sanic import Sanic\napp = Sanic('app')"))
31
+
32
+ def test_annotated_app(self):
33
+ self.assertTrue(self._check("from fastapi import FastAPI\napp: FastAPI = FastAPI()"))
34
+
35
+ def test_wsgi_function(self):
36
+ self.assertTrue(self._check("def app(environ, start_response):\n pass"))
37
+
38
+ def test_asgi_function(self):
39
+ self.assertTrue(self._check("async def app(scope, receive, send):\n pass"))
40
+
41
+ def test_imported_app(self):
42
+ self.assertTrue(self._check("from server import app"))
43
+
44
+ def test_imported_app_aliased(self):
45
+ self.assertTrue(self._check("from server import application as app"))
46
+
47
+ def test_handler_class(self):
48
+ self.assertTrue(self._check("class Handler:\n pass"))
49
+
50
+ def test_handler_class_lowercase(self):
51
+ self.assertTrue(self._check("class handler:\n pass"))
52
+
53
+ def test_no_app_or_handler(self):
54
+ self.assertFalse(self._check("def hello():\n return 'world'"))
55
+
56
+ def test_app_in_function_not_toplevel(self):
57
+ # app defined inside a function should NOT match
58
+ self.assertFalse(self._check("def create():\n app = Flask(__name__)\n return app"))
59
+
60
+ def test_syntax_error(self):
61
+ self.assertFalse(self._check("def broken("))
62
+
63
+ def test_empty_file(self):
64
+ self.assertFalse(self._check(""))
65
+
66
+ def test_only_comments(self):
67
+ self.assertFalse(self._check("# just a comment\n# another comment"))
68
+
69
+
70
+ if __name__ == "__main__":
71
+ unittest.main()
72
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/build-utils",
3
- "version": "13.2.2",
3
+ "version": "13.2.4",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.js",
@@ -26,8 +26,6 @@
26
26
  "@types/node-fetch": "^2.1.6",
27
27
  "@types/semver": "6.0.0",
28
28
  "@types/yazl": "2.4.2",
29
- "@vercel/error-utils": "2.0.3",
30
- "@vercel/routing-utils": "5.3.0",
31
29
  "aggregate-error": "3.0.1",
32
30
  "async-retry": "1.2.3",
33
31
  "async-sema": "2.1.4",
@@ -48,7 +46,9 @@
48
46
  "typescript": "4.9.5",
49
47
  "yazl": "2.5.1",
50
48
  "vitest": "2.0.1",
51
- "json5": "2.2.3"
49
+ "json5": "2.2.3",
50
+ "@vercel/error-utils": "2.0.3",
51
+ "@vercel/routing-utils": "5.3.1"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "node build.mjs",