@vercel/python 9.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
@@ -10311,7 +10311,7 @@ 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");
@@ -10320,7 +10320,14 @@ async function getFastAPIStaticDiscovery(venvPath, entrypointAbs, variableName,
10320
10320
  try {
10321
10321
  const { stderr } = await (0, import_execa9.default)(
10322
10322
  pythonPath,
10323
- [scriptPath3, variableName, outputPath, workPath, moduleName],
10323
+ [
10324
+ scriptPath3,
10325
+ variableName,
10326
+ outputPath,
10327
+ workPath,
10328
+ moduleName,
10329
+ suppressMiddlewareMounts ? "1" : "0"
10330
+ ],
10324
10331
  { env, cwd: workPath }
10325
10332
  );
10326
10333
  if (stderr) {
@@ -10384,13 +10391,14 @@ function shadowSrc(bodies) {
10384
10391
  return `^/((?:${bodies.join("|")})/?)$`;
10385
10392
  }
10386
10393
  var SHADOW_SRC_WRAPPER_LENGTH = shadowSrc([]).length;
10387
- async function runFastAPICollectStatic(venvPath, workPath, env, outputStaticDir, entrypointAbs, variableName) {
10394
+ async function runFastAPICollectStatic(venvPath, workPath, env, outputStaticDir, entrypointAbs, variableName, suppressMiddlewareMounts = true) {
10388
10395
  const { mounts, shadowRoutes } = await getFastAPIStaticDiscovery(
10389
10396
  venvPath,
10390
10397
  entrypointAbs,
10391
10398
  variableName,
10392
10399
  env,
10393
- workPath
10400
+ workPath,
10401
+ suppressMiddlewareMounts
10394
10402
  );
10395
10403
  if (mounts.length === 0) {
10396
10404
  (0, import_build_utils20.debug)("FastAPI: no StaticFiles mounts found, skipping");
@@ -10506,7 +10514,7 @@ function fastapiFallbackRoutes(discovery) {
10506
10514
  });
10507
10515
  }
10508
10516
  function getFastAPICdnBundle(collectedMounts, workPath, fastapiConfig) {
10509
- if (fastapiConfig?.static?.exclude === false)
10517
+ if (fastapiConfig?.static?.exclude !== true)
10510
10518
  return { excludePatterns: [], directoryStubs: {}, redirects: [] };
10511
10519
  const excludePatterns = [];
10512
10520
  const directoryStubs = {};
@@ -11463,7 +11471,9 @@ ${detail}`
11463
11471
  pythonEnv,
11464
11472
  outputStaticDir,
11465
11473
  entrypointAbs,
11466
- variableName
11474
+ variableName,
11475
+ // Suppress middleware mounts unless the user explicitly set cdn = true.
11476
+ staticCdn !== true
11467
11477
  );
11468
11478
  if (!fastapiStatic)
11469
11479
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/python",
3
- "version": "9.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.5.0"
20
+ "@vercel/build-utils": "14.5.1"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@renovatebot/pep440": "4.2.1",
@@ -38,7 +38,7 @@
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.0",
41
+ "@vercel/build-utils": "14.5.1",
42
42
  "@vercel/error-utils": "2.2.1",
43
43
  "@vercel/python-runtime": "0.22.1"
44
44
  },
@@ -25,8 +25,13 @@ 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
+ 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
+
28
33
  Usage:
29
- python <this_script> <variable_name> <output_path> <project_root> <module_name>
34
+ python <this_script> <variable_name> <output_path> <project_root> <module_name> <suppress_middleware_mounts>
30
35
  """
31
36
 
32
37
  from __future__ import annotations
@@ -399,7 +404,12 @@ def _subtree_shadow_route(prefix: str, mounts: list[StaticMount]) -> ShadowRoute
399
404
 
400
405
 
401
406
  def _collect_mount(
402
- 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,
403
413
  ) -> tuple[list[StaticMount], list[ShadowRoute]]:
404
414
  """Discover one app.mount(): a StaticFiles mount serves from the CDN, a
405
415
  Router/sub-app recurses, and a raw ASGI app owns its subtree opaquely.
@@ -408,11 +418,14 @@ def _collect_mount(
408
418
  if prior.eclipses(url_prefix):
409
419
  return [], []
410
420
 
421
+ if suppress_middleware_mounts:
422
+ has_middleware = has_middleware or bool(getattr(route.app, "user_middleware", []))
423
+
411
424
  mounts: list[StaticMount] = []
412
425
  shadow_routes: list[ShadowRoute] = []
413
426
 
414
427
  static = StaticMount.from_route(route, prefix, frontend=False)
415
- if static:
428
+ if static and not has_middleware:
416
429
  mounts.append(static)
417
430
  shadow_routes += prior.shadow_bodies(static)
418
431
  shadow_routes += _divergent_url_shadows(static, html=route.app.html)
@@ -428,7 +441,13 @@ def _collect_mount(
428
441
  # Recurse for the sub-app's own StaticFiles mounts, then shadow the
429
442
  # rest of its subtree to the Lambda.
430
443
  sub_prefix = prefix + route.path.rstrip("/")
431
- 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
+ )
432
451
  mounts.extend(sub_mounts)
433
452
  shadow_routes += sub_shadow
434
453
  shadow_routes.append(_subtree_shadow_route(sub_prefix, sub_mounts))
@@ -446,6 +465,9 @@ def collect(
446
465
  router: Router,
447
466
  prefix: str = "",
448
467
  prior: Precedence | None = None,
468
+ *,
469
+ has_middleware: bool,
470
+ suppress_middleware_mounts: bool,
449
471
  ) -> tuple[list[StaticMount], list[ShadowRoute]]:
450
472
  """Walk the route table for (static mounts to copy, shadow routes).
451
473
 
@@ -464,7 +486,11 @@ def collect(
464
486
  return frozenset(m) if m else None
465
487
 
466
488
  def collect_mount(mount_route: Mount) -> None:
467
- 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
+ )
468
494
  mounts.extend(sub_mounts)
469
495
  shadow_routes.extend(sub_shadow)
470
496
 
@@ -500,7 +526,7 @@ def collect(
500
526
 
501
527
  # app.include_router() with a frontend build.
502
528
  for ctx in _effective_low_priority_routes(route):
503
- if _frontend_has_dependencies(ctx):
529
+ if suppress_middleware_mounts and (_frontend_has_dependencies(ctx) or has_middleware):
504
530
  continue
505
531
  for r in ctx.original_route.routes:
506
532
  if m := StaticMount.from_route(
@@ -510,7 +536,7 @@ def collect(
510
536
 
511
537
  # app.frontend() builds.
512
538
  for group in _frontend_groups(router):
513
- if _frontend_has_dependencies(group):
539
+ if suppress_middleware_mounts and (_frontend_has_dependencies(group) or has_middleware):
514
540
  continue
515
541
  for route in group.routes:
516
542
  if m := StaticMount.from_route(route, prefix, frontend=True):
@@ -586,7 +612,7 @@ def write_output(output_path: str, discovery: Output) -> None:
586
612
  json.dump(discovery.to_dict(), f)
587
613
 
588
614
 
589
- def discover(variable_name: str, project_root: str, module_name: str) -> Output:
615
+ def discover(variable_name: str, project_root: str, module_name: str, suppress_middleware_mounts: bool) -> Output:
590
616
  """Import the entrypoint as its real module and walk its app's router.
591
617
 
592
618
  The project root is put on sys.path so importlib.import_module loads the
@@ -608,13 +634,21 @@ def discover(variable_name: str, project_root: str, module_name: str) -> Output:
608
634
  if router is None:
609
635
  return Output()
610
636
 
611
- 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
+ )
612
645
  return Output(mounts=mounts, shadowRoutes=shadow_routes)
613
646
 
614
647
 
615
648
  def main() -> None:
616
649
  variable_name, output_path, project_root, module_name = sys.argv[1:5]
617
- write_output(output_path, discover(variable_name, project_root, module_name))
650
+ suppress_middleware_mounts = sys.argv[5] != "0"
651
+ write_output(output_path, discover(variable_name, project_root, module_name, suppress_middleware_mounts))
618
652
 
619
653
 
620
654
  if __name__ == "__main__":