@vercel/build-utils 13.2.3 → 13.2.5
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/CHANGELOG.md +14 -0
- package/dist/fs/run-user-scripts.d.ts +7 -1
- package/dist/fs/run-user-scripts.js +33 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2423 -441
- package/dist/python.d.ts +22 -0
- package/dist/python.js +85 -0
- package/dist/types.d.ts +8 -0
- package/lib/python/ast_parser.py +72 -0
- package/lib/python/tests/test_ast_parser.py +72 -0
- package/package.json +2 -2
package/dist/python.d.ts
ADDED
|
@@ -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
|
+
});
|
package/dist/types.d.ts
CHANGED
|
@@ -469,6 +469,14 @@ export interface BuildResultV2Typical {
|
|
|
469
469
|
flags?: {
|
|
470
470
|
definitions: FlagDefinitions;
|
|
471
471
|
};
|
|
472
|
+
/**
|
|
473
|
+
* User-configured deployment ID for skew protection.
|
|
474
|
+
* This allows users to specify a custom deployment identifier
|
|
475
|
+
* in their next.config.js that will be used for version skew protection
|
|
476
|
+
* with pre-built deployments.
|
|
477
|
+
* @example "abc123"
|
|
478
|
+
*/
|
|
479
|
+
deploymentId?: string;
|
|
472
480
|
}
|
|
473
481
|
export type BuildResultV2 = BuildResultV2Typical | BuildResultBuildOutput;
|
|
474
482
|
export interface BuildResultV3 {
|
|
@@ -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.
|
|
3
|
+
"version": "13.2.5",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"vitest": "2.0.1",
|
|
49
49
|
"json5": "2.2.3",
|
|
50
50
|
"@vercel/error-utils": "2.0.3",
|
|
51
|
-
"@vercel/routing-utils": "5.3.
|
|
51
|
+
"@vercel/routing-utils": "5.3.2"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "node build.mjs",
|