@vercel/python 8.0.0 → 9.2.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 CHANGED
@@ -5032,7 +5032,7 @@ var import_fs20 = __toESM(require("fs"));
5032
5032
  var import_path21 = require("path");
5033
5033
 
5034
5034
  // src/package-versions.ts
5035
- var VERCEL_RUNTIME_VERSION = "0.21.1";
5035
+ var VERCEL_RUNTIME_VERSION = "0.22.1";
5036
5036
  var VERCEL_WORKERS_VERSION = "0.0.25";
5037
5037
 
5038
5038
  // src/conditional-vendoring.ts
@@ -10311,15 +10311,23 @@ var import_execa9 = __toESM(require_execa());
10311
10311
  var import_build_utils20 = require("@vercel/build-utils");
10312
10312
  var scriptPath3 = (0, import_path17.join)(__dirname, "..", "templates", "vc_fastapi_static.py");
10313
10313
  var STATIC_COLLECTION_WARNING = "Warning: FastAPI static file collection failed. Static files will not be served from the CDN.";
10314
- async function getFastAPIStaticDiscovery(venvPath, entrypointAbs, variableName, env, workPath) {
10314
+ async function getFastAPIStaticDiscovery(venvPath, entrypointAbs, variableName, env, workPath, suppressMiddlewareMounts) {
10315
10315
  const pythonPath = getVenvPythonBin(venvPath);
10316
10316
  const outputDir = (0, import_path17.join)(workPath, ".vercel", "python");
10317
10317
  const outputPath = (0, import_path17.join)(outputDir, "vc_fastapi_static_output.json");
10318
+ const moduleName = entrypointToModule((0, import_path17.relative)(workPath, entrypointAbs));
10318
10319
  await import_fs17.default.promises.mkdir(outputDir, { recursive: true });
10319
10320
  try {
10320
10321
  const { stderr } = await (0, import_execa9.default)(
10321
10322
  pythonPath,
10322
- [scriptPath3, entrypointAbs, variableName, outputPath],
10323
+ [
10324
+ scriptPath3,
10325
+ variableName,
10326
+ outputPath,
10327
+ workPath,
10328
+ moduleName,
10329
+ suppressMiddlewareMounts ? "1" : "0"
10330
+ ],
10323
10331
  { env, cwd: workPath }
10324
10332
  );
10325
10333
  if (stderr) {
@@ -10383,13 +10391,14 @@ function shadowSrc(bodies) {
10383
10391
  return `^/((?:${bodies.join("|")})/?)$`;
10384
10392
  }
10385
10393
  var SHADOW_SRC_WRAPPER_LENGTH = shadowSrc([]).length;
10386
- async function runFastAPICollectStatic(venvPath, workPath, env, outputStaticDir, entrypointAbs, variableName) {
10394
+ async function runFastAPICollectStatic(venvPath, workPath, env, outputStaticDir, entrypointAbs, variableName, suppressMiddlewareMounts = true) {
10387
10395
  const { mounts, shadowRoutes } = await getFastAPIStaticDiscovery(
10388
10396
  venvPath,
10389
10397
  entrypointAbs,
10390
10398
  variableName,
10391
10399
  env,
10392
- workPath
10400
+ workPath,
10401
+ suppressMiddlewareMounts
10393
10402
  );
10394
10403
  if (mounts.length === 0) {
10395
10404
  (0, import_build_utils20.debug)("FastAPI: no StaticFiles mounts found, skipping");
@@ -10505,7 +10514,7 @@ function fastapiFallbackRoutes(discovery) {
10505
10514
  });
10506
10515
  }
10507
10516
  function getFastAPICdnBundle(collectedMounts, workPath, fastapiConfig) {
10508
- if (fastapiConfig?.static?.exclude === false)
10517
+ if (fastapiConfig?.static?.exclude !== true)
10509
10518
  return { excludePatterns: [], directoryStubs: {}, redirects: [] };
10510
10519
  const excludePatterns = [];
10511
10520
  const directoryStubs = {};
@@ -11462,7 +11471,9 @@ ${detail}`
11462
11471
  pythonEnv,
11463
11472
  outputStaticDir,
11464
11473
  entrypointAbs,
11465
- variableName
11474
+ variableName,
11475
+ // Suppress middleware mounts unless the user explicitly set cdn = true.
11476
+ staticCdn !== true
11466
11477
  );
11467
11478
  if (!fastapiStatic)
11468
11479
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/python",
3
- "version": "8.0.0",
3
+ "version": "9.2.0",
4
4
  "main": "./dist/index.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
@@ -17,7 +17,7 @@
17
17
  "@vercel/python-analysis": "0.14.0"
18
18
  },
19
19
  "peerDependencies": {
20
- "@vercel/build-utils": "14.4.0"
20
+ "@vercel/build-utils": "14.5.1"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@renovatebot/pep440": "4.2.1",
@@ -38,9 +38,9 @@
38
38
  "smol-toml": "1.5.2",
39
39
  "vitest": "4.1.10",
40
40
  "which": "3.0.0",
41
+ "@vercel/build-utils": "14.5.1",
41
42
  "@vercel/error-utils": "2.2.1",
42
- "@vercel/build-utils": "14.4.0",
43
- "@vercel/python-runtime": "0.21.1"
43
+ "@vercel/python-runtime": "0.22.1"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "node ../../utils/build-builder.mjs",
@@ -25,12 +25,18 @@ Writes a JSON object to the output file:
25
25
  * app.mount(): routes declared BEFORE the mount win (evaluated in order).
26
26
  * app.frontend(): all normal routes win (the build is low-priority).
27
27
 
28
- Usage: python <this_script> <entrypoint_abs_path> <variable_name> <output_path>
28
+ Mounts are suppressed (omitted from output) when the app or a mounted sub-app
29
+ has user-defined middleware, because CDN-served files bypass the middleware
30
+ stack. Suppression can be disabled by passing "0" as the sixth argument (set
31
+ when [tool.vercel.fastapi.static] cdn = true in pyproject.toml).
32
+
33
+ Usage:
34
+ python <this_script> <variable_name> <output_path> <project_root> <module_name> <suppress_middleware_mounts>
29
35
  """
30
36
 
31
37
  from __future__ import annotations
32
38
 
33
- import importlib.util
39
+ import importlib
34
40
  import json
35
41
  import os
36
42
  import re
@@ -398,7 +404,12 @@ def _subtree_shadow_route(prefix: str, mounts: list[StaticMount]) -> ShadowRoute
398
404
 
399
405
 
400
406
  def _collect_mount(
401
- route: Mount, prefix: str, prior: Precedence
407
+ route: Mount,
408
+ prefix: str,
409
+ prior: Precedence,
410
+ *,
411
+ has_middleware: bool,
412
+ suppress_middleware_mounts: bool,
402
413
  ) -> tuple[list[StaticMount], list[ShadowRoute]]:
403
414
  """Discover one app.mount(): a StaticFiles mount serves from the CDN, a
404
415
  Router/sub-app recurses, and a raw ASGI app owns its subtree opaquely.
@@ -407,11 +418,14 @@ def _collect_mount(
407
418
  if prior.eclipses(url_prefix):
408
419
  return [], []
409
420
 
421
+ if suppress_middleware_mounts:
422
+ has_middleware = has_middleware or bool(getattr(route.app, "user_middleware", []))
423
+
410
424
  mounts: list[StaticMount] = []
411
425
  shadow_routes: list[ShadowRoute] = []
412
426
 
413
427
  static = StaticMount.from_route(route, prefix, frontend=False)
414
- if static:
428
+ if static and not has_middleware:
415
429
  mounts.append(static)
416
430
  shadow_routes += prior.shadow_bodies(static)
417
431
  shadow_routes += _divergent_url_shadows(static, html=route.app.html)
@@ -427,7 +441,13 @@ def _collect_mount(
427
441
  # Recurse for the sub-app's own StaticFiles mounts, then shadow the
428
442
  # rest of its subtree to the Lambda.
429
443
  sub_prefix = prefix + route.path.rstrip("/")
430
- sub_mounts, sub_shadow = collect(sub_router, sub_prefix, prior)
444
+ sub_mounts, sub_shadow = collect(
445
+ sub_router,
446
+ sub_prefix,
447
+ prior,
448
+ has_middleware=has_middleware,
449
+ suppress_middleware_mounts=suppress_middleware_mounts,
450
+ )
431
451
  mounts.extend(sub_mounts)
432
452
  shadow_routes += sub_shadow
433
453
  shadow_routes.append(_subtree_shadow_route(sub_prefix, sub_mounts))
@@ -445,6 +465,9 @@ def collect(
445
465
  router: Router,
446
466
  prefix: str = "",
447
467
  prior: Precedence | None = None,
468
+ *,
469
+ has_middleware: bool,
470
+ suppress_middleware_mounts: bool,
448
471
  ) -> tuple[list[StaticMount], list[ShadowRoute]]:
449
472
  """Walk the route table for (static mounts to copy, shadow routes).
450
473
 
@@ -463,7 +486,11 @@ def collect(
463
486
  return frozenset(m) if m else None
464
487
 
465
488
  def collect_mount(mount_route: Mount) -> None:
466
- sub_mounts, sub_shadow = _collect_mount(mount_route, prefix, prior)
489
+ sub_mounts, sub_shadow = _collect_mount(
490
+ mount_route, prefix, prior,
491
+ has_middleware=has_middleware,
492
+ suppress_middleware_mounts=suppress_middleware_mounts,
493
+ )
467
494
  mounts.extend(sub_mounts)
468
495
  shadow_routes.extend(sub_shadow)
469
496
 
@@ -499,7 +526,7 @@ def collect(
499
526
 
500
527
  # app.include_router() with a frontend build.
501
528
  for ctx in _effective_low_priority_routes(route):
502
- if _frontend_has_dependencies(ctx):
529
+ if suppress_middleware_mounts and (_frontend_has_dependencies(ctx) or has_middleware):
503
530
  continue
504
531
  for r in ctx.original_route.routes:
505
532
  if m := StaticMount.from_route(
@@ -509,7 +536,7 @@ def collect(
509
536
 
510
537
  # app.frontend() builds.
511
538
  for group in _frontend_groups(router):
512
- if _frontend_has_dependencies(group):
539
+ if suppress_middleware_mounts and (_frontend_has_dependencies(group) or has_middleware):
513
540
  continue
514
541
  for route in group.routes:
515
542
  if m := StaticMount.from_route(route, prefix, frontend=True):
@@ -585,17 +612,21 @@ def write_output(output_path: str, discovery: Output) -> None:
585
612
  json.dump(discovery.to_dict(), f)
586
613
 
587
614
 
588
- def discover(entrypoint_abs: str, variable_name: str) -> Output:
589
- """Import the entrypoint and walk its app's router; empty on any failure."""
590
- spec = importlib.util.spec_from_file_location("__vc_app", entrypoint_abs)
591
- if spec is None or spec.loader is None:
592
- return Output()
615
+ def discover(variable_name: str, project_root: str, module_name: str, suppress_middleware_mounts: bool) -> Output:
616
+ """Import the entrypoint as its real module and walk its app's router.
617
+
618
+ The project root is put on sys.path so importlib.import_module loads the
619
+ entrypoint by its dotted name (as the deployed runtime does), which is what
620
+ makes relative and first-party absolute imports resolve. Returns empty on
621
+ any failure.
622
+ """
623
+ if project_root and project_root not in sys.path:
624
+ sys.path.insert(0, project_root)
593
625
 
594
- mod = importlib.util.module_from_spec(spec)
595
626
  try:
596
- spec.loader.exec_module(mod) # type: ignore[union-attr]
627
+ mod = importlib.import_module(module_name)
597
628
  except Exception as exc:
598
- print(f"vc_fastapi_static: exec_module failed: {exc}", file=sys.stderr)
629
+ print(f"vc_fastapi_static: import failed: {exc}", file=sys.stderr)
599
630
  return Output()
600
631
 
601
632
  app = getattr(mod, variable_name, None)
@@ -603,13 +634,21 @@ def discover(entrypoint_abs: str, variable_name: str) -> Output:
603
634
  if router is None:
604
635
  return Output()
605
636
 
606
- mounts, shadow_routes = collect(router)
637
+ # If the top-level app has middleware, no mounts can be promoted —
638
+ # CDN-served files would bypass the middleware stack entirely.
639
+ if suppress_middleware_mounts and getattr(app, "user_middleware", []):
640
+ return Output()
641
+
642
+ mounts, shadow_routes = collect(
643
+ router, has_middleware=False, suppress_middleware_mounts=suppress_middleware_mounts
644
+ )
607
645
  return Output(mounts=mounts, shadowRoutes=shadow_routes)
608
646
 
609
647
 
610
648
  def main() -> None:
611
- entrypoint_abs, variable_name, output_path = sys.argv[1:4]
612
- write_output(output_path, discover(entrypoint_abs, variable_name))
649
+ variable_name, output_path, project_root, module_name = sys.argv[1:5]
650
+ suppress_middleware_mounts = sys.argv[5] != "0"
651
+ write_output(output_path, discover(variable_name, project_root, module_name, suppress_middleware_mounts))
613
652
 
614
653
 
615
654
  if __name__ == "__main__":