@vercel/build-utils 13.3.0 → 13.3.2

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/python.d.ts CHANGED
@@ -1,17 +1,4 @@
1
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
2
  /**
16
3
  * Check if a Python file is a valid entrypoint by detecting:
17
4
  * - A top-level 'app' callable (Flask, FastAPI, Sanic, WSGI/ASGI, etc.)
package/dist/python.js CHANGED
@@ -28,51 +28,19 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var python_exports = {};
30
30
  __export(python_exports, {
31
- isPythonEntrypoint: () => isPythonEntrypoint,
32
- runStdlibPyScript: () => runStdlibPyScript
31
+ isPythonEntrypoint: () => isPythonEntrypoint
33
32
  });
34
33
  module.exports = __toCommonJS(python_exports);
35
34
  var import_fs = __toESM(require("fs"));
36
- var import_path = require("path");
37
- var import_execa = __toESM(require("execa"));
35
+ var import_python_analysis = require("@vercel/python-analysis");
38
36
  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
37
  async function isPythonEntrypoint(file) {
63
38
  try {
64
39
  const fsPath = file.fsPath;
65
40
  if (!fsPath)
66
41
  return false;
67
42
  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;
43
+ return await (0, import_python_analysis.containsAppOrHandler)(content);
76
44
  } catch (err) {
77
45
  (0, import_debug.default)(`Failed to check Python entrypoint: ${err}`);
78
46
  return false;
@@ -80,6 +48,5 @@ async function isPythonEntrypoint(file) {
80
48
  }
81
49
  // Annotate the CommonJS export names for ESM import in node:
82
50
  0 && (module.exports = {
83
- isPythonEntrypoint,
84
- runStdlibPyScript
51
+ isPythonEntrypoint
85
52
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/build-utils",
3
- "version": "13.3.0",
3
+ "version": "13.3.2",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.js",
@@ -10,6 +10,9 @@
10
10
  "url": "https://github.com/vercel/vercel.git",
11
11
  "directory": "packages/now-build-utils"
12
12
  },
13
+ "dependencies": {
14
+ "@vercel/python-analysis": "0.3.1"
15
+ },
13
16
  "devDependencies": {
14
17
  "@iarna/toml": "2.2.3",
15
18
  "@types/async-retry": "^1.2.1",
@@ -47,8 +50,8 @@
47
50
  "yazl": "2.5.1",
48
51
  "vitest": "2.0.1",
49
52
  "json5": "2.2.3",
50
- "@vercel/error-utils": "2.0.3",
51
- "@vercel/routing-utils": "5.3.2"
53
+ "@vercel/routing-utils": "5.3.2",
54
+ "@vercel/error-utils": "2.0.3"
52
55
  },
53
56
  "scripts": {
54
57
  "build": "node build.mjs",
@@ -1,72 +0,0 @@
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
-
@@ -1,72 +0,0 @@
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
-