Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,8 @@ def _compile(
ReflexRuntimeError: When any page uses state, but no rx.State subclass is defined.
FileNotFoundError: When a plugin requires a file that does not exist.
"""
from reflex import route as route_module
from reflex.utils import frontend_skeleton
from reflex.utils.exceptions import ReflexRuntimeError

self._apply_decorated_pages()
Expand Down Expand Up @@ -1534,6 +1536,13 @@ def _submit_work_without_advancing(
raise FileNotFoundError(msg)
output_mapping[path] = modify_fn(file_content)

# Update package.json with dynamic route patterns for sirv --ignores
routes = list(self._unevaluated_pages.keys())
dynamic_route_prefixes = route_module.get_dynamic_route_prefixes(routes)
if dynamic_route_prefixes:
console.debug(f"Updating package.json with dynamic route prefixes: {dynamic_route_prefixes}")
frontend_skeleton.initialize_package_json(dynamic_route_prefixes)

with console.timing("Write to Disk"):
for output_path, code in output_mapping.items():
compiler_utils.write_file(output_path, code)
Expand Down
50 changes: 50 additions & 0 deletions reflex/route.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,56 @@ def replace_brackets_with_keywords(input_string: str) -> str:
)


def get_dynamic_route_prefixes(routes: list[str]) -> list[str]:
"""Get route prefixes for dynamic routes to use with sirv --ignores.

sirv treats URLs with extensions (periods) as asset requests, not SPA routes.
This function extracts route patterns that have dynamic segments, so they can
be excluded from asset detection using sirv's --ignores flag.

Example:
>>> get_dynamic_route_prefixes(["/posts/[slug]", "/data/[version]/info"])
['^/posts/', '^/data/']

Args:
routes: List of route strings.

Returns:
List of regex patterns for sirv --ignores flag.
"""
prefixes = []
for route in routes:
# Check if route has dynamic segments
if get_route_args(route):
# Extract the prefix up to the first dynamic segment
parts = route.split("/")
prefix_parts = []
for part in parts:
# Stop at first dynamic segment
if (
constants.RouteRegex.ARG.match(part)
or constants.RouteRegex.OPTIONAL_ARG.match(part)
or constants.RouteRegex.STRICT_CATCHALL.match(part)
or constants.RouteRegex.OPTIONAL_CATCHALL.match(part)
or part == constants.RouteRegex.SPLAT_CATCHALL
):
break
prefix_parts.append(part)

# Build prefix pattern (e.g., "/posts" -> "^/posts/")
prefix = "/".join(prefix_parts)
# Ensure prefix starts with / for absolute path matching
if not prefix.startswith("/"):
prefix = "/" + prefix
if prefix and prefix != "/":
# Add trailing slash and anchor to start for sirv pattern
pattern = f"^{prefix}/"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use re.escape(prefix) here to ensure the prefix will never contain regex injection

if pattern not in prefixes:
prefixes.append(pattern)

return prefixes


def route_specificity(keyworded_route: str) -> tuple[int, int, int]:
"""Get the specificity of a route with keywords.

Expand Down
26 changes: 21 additions & 5 deletions reflex/utils/frontend_skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,23 +167,39 @@ def _update_react_router_config(config: Config, prerender_routes: bool = False):
return f"export default {json.dumps(react_router_config)};"


def _compile_package_json():
def _compile_package_json(dynamic_route_prefixes: list[str] | None = None):
"""Compile package.json with optional dynamic route handling.

Args:
dynamic_route_prefixes: List of route prefix patterns for sirv --ignores.
"""
# Build prod command with --ignores flags for dynamic routes
prod_command = constants.PackageJson.Commands.PROD
if dynamic_route_prefixes:
# Add --ignores flag for each dynamic route prefix
ignores = " ".join(f"--ignores '{prefix}'" for prefix in dynamic_route_prefixes)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use shlex.join instead so these values get properly shell quoted

prod_command = f"{prod_command} {ignores}"

return templates.package_json_template(
scripts={
"dev": constants.PackageJson.Commands.DEV,
"export": constants.PackageJson.Commands.EXPORT,
"prod": constants.PackageJson.Commands.PROD,
"prod": prod_command,
},
dependencies=constants.PackageJson.DEPENDENCIES,
dev_dependencies=constants.PackageJson.DEV_DEPENDENCIES,
overrides=constants.PackageJson.OVERRIDES,
)


def initialize_package_json():
"""Render and write in .web the package.json file."""
def initialize_package_json(dynamic_route_prefixes: list[str] | None = None):
"""Render and write in .web the package.json file.

Args:
dynamic_route_prefixes: List of route prefix patterns for sirv --ignores.
"""
output_path = get_web_dir() / constants.PackageJson.PATH
output_path.write_text(_compile_package_json())
output_path.write_text(_compile_package_json(dynamic_route_prefixes))


def _compile_vite_config(config: Config):
Expand Down