diff --git a/.coveragerc b/.coveragerc
index 67e9b345a..1f383608e 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -10,7 +10,7 @@ omit =
[report]
show_missing = true
# TODO bump back to 79
-fail_under = 72
+fail_under = 70
precision = 2
# Regexes for lines to exclude from consideration
diff --git a/integration/init-test/in_docker_test_script.sh b/integration/init-test/in_docker_test_script.sh
index 733cbf4ad..4fb836fd1 100755
--- a/integration/init-test/in_docker_test_script.sh
+++ b/integration/init-test/in_docker_test_script.sh
@@ -34,4 +34,3 @@ redis-server &
echo "Running reflex init in test project dir"
do_export blank
-do_export sidebar
\ No newline at end of file
diff --git a/reflex/.templates/apps/sidebar/README.md b/reflex/.templates/apps/sidebar/README.md
deleted file mode 100644
index d80145d34..000000000
--- a/reflex/.templates/apps/sidebar/README.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# Welcome to Reflex!
-
-This is the base Reflex template - installed when you run `reflex init`.
-
-If you want to use a different template, pass the `--template` flag to `reflex init`.
-For example, if you want a more basic starting point, you can run:
-
-```bash
-reflex init --template blank
-```
-
-## About this Template
-
-This template has the following directory structure:
-
-```bash
-├── README.md
-├── assets
-├── rxconfig.py
-└── {your_app}
- ├── __init__.py
- ├── components
- │ ├── __init__.py
- │ └── sidebar.py
- ├── pages
- │ ├── __init__.py
- │ ├── dashboard.py
- │ ├── index.py
- │ └── settings.py
- ├── styles.py
- ├── templates
- │ ├── __init__.py
- │ └── template.py
- └── {your_app}.py
-```
-
-See the [Project Structure docs](https://reflex.dev/docs/getting-started/project-structure/) for more information on general Reflex project structure.
-
-### Adding Pages
-
-In this template, the pages in your app are defined in `{your_app}/pages/`.
-Each page is a function that returns a Reflex component.
-For example, to edit this page you can modify `{your_app}/pages/index.py`.
-See the [pages docs](https://reflex.dev/docs/pages/routes/) for more information on pages.
-
-In this template, instead of using `rx.add_page` or the `@rx.page` decorator,
-we use the `@template` decorator from `{your_app}/templates/template.py`.
-
-To add a new page:
-
-1. Add a new file in `{your_app}/pages/`. We recommend using one file per page, but you can also group pages in a single file.
-2. Add a new function with the `@template` decorator, which takes the same arguments as `@rx.page`.
-3. Import the page in your `{your_app}/pages/__init__.py` file and it will automatically be added to the app.
-
-
-### Adding Components
-
-In order to keep your code organized, we recommend putting components that are
-used across multiple pages in the `{your_app}/components/` directory.
-
-In this template, we have a sidebar component in `{your_app}/components/sidebar.py`.
-
-### Adding State
-
-As your app grows, we recommend using [substates](https://reflex.dev/docs/substates/overview/)
-to organize your state.
-
-You can either define substates in their own files, or if the state is
-specific to a page, you can define it in the page file itself.
diff --git a/reflex/.templates/apps/sidebar/assets/favicon.ico b/reflex/.templates/apps/sidebar/assets/favicon.ico
deleted file mode 100644
index 166ae995e..000000000
Binary files a/reflex/.templates/apps/sidebar/assets/favicon.ico and /dev/null differ
diff --git a/reflex/.templates/apps/sidebar/assets/github.svg b/reflex/.templates/apps/sidebar/assets/github.svg
deleted file mode 100644
index 61c9d791b..000000000
--- a/reflex/.templates/apps/sidebar/assets/github.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/reflex/.templates/apps/sidebar/assets/logo.svg b/reflex/.templates/apps/sidebar/assets/logo.svg
deleted file mode 100644
index 94fe1f511..000000000
--- a/reflex/.templates/apps/sidebar/assets/logo.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/reflex/.templates/apps/sidebar/assets/paneleft.svg b/reflex/.templates/apps/sidebar/assets/paneleft.svg
deleted file mode 100644
index ac9c5040a..000000000
--- a/reflex/.templates/apps/sidebar/assets/paneleft.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
diff --git a/reflex/.templates/apps/sidebar/assets/reflex_black.svg b/reflex/.templates/apps/sidebar/assets/reflex_black.svg
deleted file mode 100644
index b9cc89da9..000000000
--- a/reflex/.templates/apps/sidebar/assets/reflex_black.svg
+++ /dev/null
@@ -1,37 +0,0 @@
-
diff --git a/reflex/.templates/apps/sidebar/assets/reflex_white.svg b/reflex/.templates/apps/sidebar/assets/reflex_white.svg
deleted file mode 100644
index 63876bdd1..000000000
--- a/reflex/.templates/apps/sidebar/assets/reflex_white.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
diff --git a/reflex/.templates/apps/sidebar/code/__init__.py b/reflex/.templates/apps/sidebar/code/__init__.py
deleted file mode 100644
index e1d286346..000000000
--- a/reflex/.templates/apps/sidebar/code/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Base template for Reflex."""
diff --git a/reflex/.templates/apps/sidebar/code/components/__init__.py b/reflex/.templates/apps/sidebar/code/components/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/reflex/.templates/apps/sidebar/code/components/sidebar.py b/reflex/.templates/apps/sidebar/code/components/sidebar.py
deleted file mode 100644
index fbdc56db4..000000000
--- a/reflex/.templates/apps/sidebar/code/components/sidebar.py
+++ /dev/null
@@ -1,141 +0,0 @@
-"""Sidebar component for the app."""
-
-from code import styles
-
-import reflex as rx
-
-
-def sidebar_header() -> rx.Component:
- """Sidebar header.
-
- Returns:
- The sidebar header component.
- """
- return rx.hstack(
- # The logo.
- rx.color_mode_cond(
- rx.image(src="/reflex_black.svg", height="2em"),
- rx.image(src="/reflex_white.svg", height="2em"),
- ),
- rx.spacer(),
- rx.link(
- rx.button(
- rx.icon("github"),
- color_scheme="gray",
- variant="soft",
- ),
- href="https://github.com/reflex-dev/reflex",
- ),
- align="center",
- width="100%",
- border_bottom=styles.border,
- padding_x="1em",
- padding_y="2em",
- )
-
-
-def sidebar_footer() -> rx.Component:
- """Sidebar footer.
-
- Returns:
- The sidebar footer component.
- """
- return rx.hstack(
- rx.spacer(),
- rx.link(
- rx.text("Docs"),
- href="https://reflex.dev/docs/getting-started/introduction/",
- color_scheme="gray",
- ),
- rx.link(
- rx.text("Blog"),
- href="https://reflex.dev/blog/",
- color_scheme="gray",
- ),
- width="100%",
- border_top=styles.border,
- padding="1em",
- )
-
-
-def sidebar_item(text: str, url: str) -> rx.Component:
- """Sidebar item.
-
- Args:
- text: The text of the item.
- url: The URL of the item.
-
- Returns:
- rx.Component: The sidebar item component.
- """
- # Whether the item is active.
- active = (rx.State.router.page.path == f"/{text.lower()}") | (
- (rx.State.router.page.path == "/") & text == "Home"
- )
-
- return rx.link(
- rx.hstack(
- rx.text(
- text,
- ),
- bg=rx.cond(
- active,
- rx.color("accent", 2),
- "transparent",
- ),
- border=rx.cond(
- active,
- f"1px solid {rx.color('accent', 6)}",
- f"1px solid {rx.color('gray', 6)}",
- ),
- color=rx.cond(
- active,
- styles.accent_text_color,
- styles.text_color,
- ),
- align="center",
- border_radius=styles.border_radius,
- width="100%",
- padding="1em",
- ),
- href=url,
- width="100%",
- )
-
-
-def sidebar() -> rx.Component:
- """The sidebar.
-
- Returns:
- The sidebar component.
- """
- # Get all the decorated pages and add them to the sidebar.
- from reflex.page import get_decorated_pages
-
- return rx.box(
- rx.vstack(
- sidebar_header(),
- rx.vstack(
- *[
- sidebar_item(
- text=page.get("title", page["route"].strip("/").capitalize()),
- url=page["route"],
- )
- for page in get_decorated_pages()
- ],
- width="100%",
- overflow_y="auto",
- align_items="flex-start",
- padding="1em",
- ),
- rx.spacer(),
- sidebar_footer(),
- height="100dvh",
- ),
- display=["none", "none", "block"],
- min_width=styles.sidebar_width,
- height="100%",
- position="sticky",
- top="0px",
- border_right=styles.border,
- )
diff --git a/reflex/.templates/apps/sidebar/code/pages/__init__.py b/reflex/.templates/apps/sidebar/code/pages/__init__.py
deleted file mode 100644
index 8e5da1a38..000000000
--- a/reflex/.templates/apps/sidebar/code/pages/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .dashboard import dashboard
-from .index import index
-from .settings import settings
diff --git a/reflex/.templates/apps/sidebar/code/pages/dashboard.py b/reflex/.templates/apps/sidebar/code/pages/dashboard.py
deleted file mode 100644
index f64f9158d..000000000
--- a/reflex/.templates/apps/sidebar/code/pages/dashboard.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""The dashboard page."""
-
-from code.templates import template
-
-import reflex as rx
-
-
-@template(route="/dashboard", title="Dashboard")
-def dashboard() -> rx.Component:
- """The dashboard page.
-
- Returns:
- The UI for the dashboard page.
- """
- return rx.vstack(
- rx.heading("Dashboard", size="8"),
- rx.text("Welcome to Reflex!"),
- rx.text(
- "You can edit this page in ",
- rx.code("{your_app}/pages/dashboard.py"),
- ),
- )
diff --git a/reflex/.templates/apps/sidebar/code/pages/index.py b/reflex/.templates/apps/sidebar/code/pages/index.py
deleted file mode 100644
index e79ab75c2..000000000
--- a/reflex/.templates/apps/sidebar/code/pages/index.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""The home page of the app."""
-
-from code import styles
-from code.templates import template
-
-import reflex as rx
-
-
-@template(route="/", title="Home")
-def index() -> rx.Component:
- """The home page.
-
- Returns:
- The UI for the home page.
- """
- with open("README.md", encoding="utf-8") as readme:
- content = readme.read()
- return rx.markdown(content, component_map=styles.markdown_style)
diff --git a/reflex/.templates/apps/sidebar/code/pages/settings.py b/reflex/.templates/apps/sidebar/code/pages/settings.py
deleted file mode 100644
index 1272c8fe7..000000000
--- a/reflex/.templates/apps/sidebar/code/pages/settings.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""The settings page."""
-
-from code.templates import ThemeState, template
-
-import reflex as rx
-
-
-@template(route="/settings", title="Settings")
-def settings() -> rx.Component:
- """The settings page.
-
- Returns:
- The UI for the settings page.
- """
- return rx.vstack(
- rx.heading("Settings", size="8"),
- rx.hstack(
- rx.text("Dark mode: "),
- rx.color_mode.switch(),
- ),
- rx.hstack(
- rx.text("Primary color: "),
- rx.select(
- [
- "tomato",
- "red",
- "ruby",
- "crimson",
- "pink",
- "plum",
- "purple",
- "violet",
- "iris",
- "indigo",
- "blue",
- "cyan",
- "teal",
- "jade",
- "green",
- "grass",
- "brown",
- "orange",
- "sky",
- "mint",
- "lime",
- "yellow",
- "amber",
- "gold",
- "bronze",
- "gray",
- ],
- value=ThemeState.accent_color,
- on_change=ThemeState.set_accent_color,
- ),
- ),
- rx.hstack(
- rx.text("Secondary color: "),
- rx.select(
- [
- "gray",
- "mauve",
- "slate",
- "sage",
- "olive",
- "sand",
- ],
- value=ThemeState.gray_color,
- on_change=ThemeState.set_gray_color,
- ),
- ),
- rx.text(
- "You can edit this page in ",
- rx.code("{your_app}/pages/settings.py"),
- size="1",
- ),
- )
diff --git a/reflex/.templates/apps/sidebar/code/sidebar.py b/reflex/.templates/apps/sidebar/code/sidebar.py
deleted file mode 100644
index 8b1e2c224..000000000
--- a/reflex/.templates/apps/sidebar/code/sidebar.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""Welcome to Reflex!."""
-
-# Import all the pages.
-from code.pages import *
-
-import reflex as rx
-
-
-class State(rx.State):
- """Define empty state to allow access to rx.State.router."""
-
-
-# Create the app.
-app = rx.App()
diff --git a/reflex/.templates/apps/sidebar/code/styles.py b/reflex/.templates/apps/sidebar/code/styles.py
deleted file mode 100644
index e999c8845..000000000
--- a/reflex/.templates/apps/sidebar/code/styles.py
+++ /dev/null
@@ -1,45 +0,0 @@
-"""Styles for the app."""
-
-import reflex as rx
-
-border_radius = "0.375rem"
-border = f"1px solid {rx.color('gray', 6)}"
-text_color = rx.color("gray", 11)
-accent_text_color = rx.color("accent", 10)
-accent_color = rx.color("accent", 1)
-hover_accent_color = {"_hover": {"color": accent_text_color}}
-hover_accent_bg = {"_hover": {"background_color": accent_color}}
-content_width_vw = "90vw"
-sidebar_width = "20em"
-
-template_page_style = {"padding_top": "5em", "padding_x": ["auto", "2em"], "flex": "1"}
-
-template_content_style = {
- "align_items": "flex-start",
- "border_radius": border_radius,
- "padding": "1em",
- "margin_bottom": "2em",
-}
-
-link_style = {
- "color": accent_text_color,
- "text_decoration": "none",
- **hover_accent_color,
-}
-
-overlapping_button_style = {
- "background_color": "white",
- "border_radius": border_radius,
-}
-
-markdown_style = {
- "code": lambda text: rx.code(text, color_scheme="gray"),
- "codeblock": lambda text, **props: rx.code_block(text, **props, margin_y="1em"),
- "a": lambda text, **props: rx.link(
- text,
- **props,
- font_weight="bold",
- text_decoration="underline",
- text_decoration_color=accent_text_color,
- ),
-}
diff --git a/reflex/.templates/apps/sidebar/code/templates/__init__.py b/reflex/.templates/apps/sidebar/code/templates/__init__.py
deleted file mode 100644
index 54af7bc09..000000000
--- a/reflex/.templates/apps/sidebar/code/templates/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .template import ThemeState, template
diff --git a/reflex/.templates/apps/sidebar/code/templates/template.py b/reflex/.templates/apps/sidebar/code/templates/template.py
deleted file mode 100644
index c1c3acf63..000000000
--- a/reflex/.templates/apps/sidebar/code/templates/template.py
+++ /dev/null
@@ -1,144 +0,0 @@
-"""Common templates used between pages in the app."""
-
-from __future__ import annotations
-
-from code import styles
-from code.components.sidebar import sidebar
-from typing import Callable
-
-import reflex as rx
-
-# Meta tags for the app.
-default_meta = [
- {
- "name": "viewport",
- "content": "width=device-width, shrink-to-fit=no, initial-scale=1",
- },
-]
-
-
-def menu_item_link(text, href):
- return rx.menu.item(
- rx.link(
- text,
- href=href,
- width="100%",
- color="inherit",
- ),
- _hover={
- "color": styles.accent_color,
- "background_color": styles.accent_text_color,
- },
- )
-
-
-def menu_button() -> rx.Component:
- """The menu button on the top right of the page.
-
- Returns:
- The menu button component.
- """
- from reflex.page import get_decorated_pages
-
- return rx.box(
- rx.menu.root(
- rx.menu.trigger(
- rx.button(
- rx.icon("menu"),
- variant="soft",
- )
- ),
- rx.menu.content(
- *[
- menu_item_link(page["title"], page["route"])
- for page in get_decorated_pages()
- ],
- rx.menu.separator(),
- menu_item_link("About", "https://github.com/reflex-dev"),
- menu_item_link("Contact", "mailto:founders@=reflex.dev"),
- ),
- ),
- position="fixed",
- right="2em",
- top="2em",
- z_index="500",
- )
-
-
-class ThemeState(rx.State):
- """The state for the theme of the app."""
-
- accent_color: str = "crimson"
-
- gray_color: str = "gray"
-
-
-def template(
- route: str | None = None,
- title: str | None = None,
- description: str | None = None,
- meta: str | None = None,
- script_tags: list[rx.Component] | None = None,
- on_load: rx.event.EventHandler | list[rx.event.EventHandler] | None = None,
-) -> Callable[[Callable[[], rx.Component]], rx.Component]:
- """The template for each page of the app.
-
- Args:
- route: The route to reach the page.
- title: The title of the page.
- description: The description of the page.
- meta: Additionnal meta to add to the page.
- on_load: The event handler(s) called when the page load.
- script_tags: Scripts to attach to the page.
-
- Returns:
- The template with the page content.
- """
-
- def decorator(page_content: Callable[[], rx.Component]) -> rx.Component:
- """The template for each page of the app.
-
- Args:
- page_content: The content of the page.
-
- Returns:
- The template with the page content.
- """
- # Get the meta tags for the page.
- all_meta = [*default_meta, *(meta or [])]
-
- def templated_page():
- return rx.hstack(
- sidebar(),
- rx.box(
- rx.box(
- page_content(),
- **styles.template_content_style,
- ),
- **styles.template_page_style,
- ),
- menu_button(),
- align="start",
- background=f"radial-gradient(circle at top right, {rx.color('accent', 2)}, {rx.color('mauve', 1)});",
- position="relative",
- )
-
- @rx.page(
- route=route,
- title=title,
- description=description,
- meta=all_meta,
- script_tags=script_tags,
- on_load=on_load,
- )
- def theme_wrap():
- return rx.theme(
- templated_page(),
- has_background=True,
- accent_color=ThemeState.accent_color,
- gray_color=ThemeState.gray_color,
- )
-
- return theme_wrap
-
- return decorator
diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js
index 012ff0c3c..95f7c5a05 100644
--- a/reflex/.templates/web/utils/state.js
+++ b/reflex/.templates/web/utils/state.js
@@ -646,7 +646,12 @@ export const useEventLoop = (
// Route after the initial page hydration.
useEffect(() => {
- const change_start = () => dispatch["state"]({is_hydrated: false})
+ const change_start = () => {
+ const main_state_dispatch = dispatch["state"]
+ if (main_state_dispatch !== undefined) {
+ main_state_dispatch({is_hydrated: false})
+ }
+ }
const change_complete = () => addEvents(onLoadInternalEvent());
router.events.on("routeChangeStart", change_start);
router.events.on("routeChangeComplete", change_complete);
diff --git a/reflex/constants/base.py b/reflex/constants/base.py
index 11d73a7c1..733859ad4 100644
--- a/reflex/constants/base.py
+++ b/reflex/constants/base.py
@@ -88,12 +88,11 @@ class ReflexHostingCLI(SimpleNamespace):
class Templates(SimpleNamespace):
"""Constants related to Templates."""
- # Dynamically get the enum values from the .templates folder
- template_dir = os.path.join(Reflex.ROOT_DIR, Reflex.MODULE_NAME, ".templates/apps")
- template_dirs = next(os.walk(template_dir))[1]
+ # The route on Reflex backend to query which templates are available and their URLs.
+ APP_TEMPLATES_ROUTE = "/app-templates"
- # Create an enum value for each directory in the .templates folder
- Kind = Enum("Kind", {template.upper(): template for template in template_dirs})
+ # The default template
+ DEFAULT = "blank"
class Dirs(SimpleNamespace):
"""Folders used by the template system of Reflex."""
diff --git a/reflex/constants/base.pyi b/reflex/constants/base.pyi
new file mode 100644
index 000000000..90804a080
--- /dev/null
+++ b/reflex/constants/base.pyi
@@ -0,0 +1,94 @@
+"""Stub file for reflex/constants/base.py"""
+# ------------------- DO NOT EDIT ----------------------
+# This file was generated by `reflex/utils/pyi_generator.py`!
+# ------------------------------------------------------
+
+from typing import Any, Dict, Literal, Optional, Union, overload
+from reflex.vars import Var, BaseVar, ComputedVar
+from reflex.event import EventChain, EventHandler, EventSpec
+from reflex.style import Style
+import os
+import platform
+from enum import Enum
+from importlib import metadata
+from types import SimpleNamespace
+from platformdirs import PlatformDirs
+
+IS_WINDOWS = platform.system() == "Windows"
+
+class Dirs(SimpleNamespace):
+ WEB = ".web"
+ APP_ASSETS = "assets"
+ UTILS = "utils"
+ STATIC = "_static"
+ STATE_PATH = "/".join([UTILS, "state"])
+ COMPONENTS_PATH = "/".join([UTILS, "components"])
+ CONTEXTS_PATH = "/".join([UTILS, "context"])
+ WEB_PAGES = os.path.join(WEB, "pages")
+ WEB_STATIC = os.path.join(WEB, STATIC)
+ WEB_UTILS = os.path.join(WEB, UTILS)
+ WEB_ASSETS = os.path.join(WEB, "public")
+ ENV_JSON = os.path.join(WEB, "env.json")
+ REFLEX_JSON = os.path.join(WEB, "reflex.json")
+ POSTCSS_JS = os.path.join(WEB, "postcss.config.js")
+
+class Reflex(SimpleNamespace):
+ MODULE_NAME = "reflex"
+ VERSION = metadata.version(MODULE_NAME)
+ JSON = os.path.join(Dirs.WEB, "reflex.json")
+ _dir = os.environ.get("REFLEX_DIR", "")
+ DIR = _dir or PlatformDirs(MODULE_NAME, False).user_data_dir
+ ROOT_DIR = os.path.dirname(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ )
+
+class ReflexHostingCLI(SimpleNamespace):
+ MODULE_NAME = "reflex-hosting-cli"
+
+class Templates(SimpleNamespace):
+ APP_TEMPLATES_ROUTE = "/app-templates"
+ DEFAULT = "blank"
+
+ class Dirs(SimpleNamespace):
+ BASE = os.path.join(Reflex.ROOT_DIR, Reflex.MODULE_NAME, ".templates")
+ WEB_TEMPLATE = os.path.join(BASE, "web")
+ JINJA_TEMPLATE = os.path.join(BASE, "jinja")
+ CODE = "code"
+
+class Next(SimpleNamespace):
+ CONFIG_FILE = "next.config.js"
+ SITEMAP_CONFIG_FILE = os.path.join(Dirs.WEB, "next-sitemap.config.js")
+ NODE_MODULES = "node_modules"
+ PACKAGE_LOCK = "package-lock.json"
+ FRONTEND_LISTENING_REGEX = "Local:[\\s]+(.*)"
+
+class ColorMode(SimpleNamespace):
+ NAME = "colorMode"
+ USE = "useColorMode"
+ TOGGLE = "toggleColorMode"
+
+class Env(str, Enum):
+ DEV = "dev"
+ PROD = "prod"
+
+class LogLevel(str, Enum):
+ DEBUG = "debug"
+ INFO = "info"
+ WARNING = "warning"
+ ERROR = "error"
+ CRITICAL = "critical"
+
+POLLING_MAX_HTTP_BUFFER_SIZE = 1000 * 1000
+
+class Ping(SimpleNamespace):
+ INTERVAL = 25
+ TIMEOUT = 120
+
+COOKIES = "cookies"
+LOCAL_STORAGE = "local_storage"
+SKIP_COMPILE_ENV_VAR = "__REFLEX_SKIP_COMPILE"
+ENV_MODE_ENV_VAR = "REFLEX_ENV_MODE"
+PYTEST_CURRENT_TEST = "PYTEST_CURRENT_TEST"
+RELOAD_CONFIG = "__REFLEX_RELOAD_CONFIG"
+REFLEX_VAR_OPENING_TAG = ""
+REFLEX_VAR_CLOSING_TAG = ""
diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py
index 40bd3a8d9..030942c50 100644
--- a/reflex/custom_components/custom_components.py
+++ b/reflex/custom_components/custom_components.py
@@ -145,7 +145,7 @@ def _populate_demo_app(name_variants: NameVariants):
with set_directory(demo_app_dir):
# We start with the blank template as basis.
- _init(name=demo_app_name, template=constants.Templates.Kind.BLANK)
+ _init(name=demo_app_name, template=constants.Templates.DEFAULT)
# Then overwrite the app source file with the one we want for testing custom components.
# This source file is rendered using jinja template file.
with open(f"{demo_app_name}/{demo_app_name}.py", "w") as f:
diff --git a/reflex/reflex.py b/reflex/reflex.py
index e7496c2ea..754d96af9 100644
--- a/reflex/reflex.py
+++ b/reflex/reflex.py
@@ -63,7 +63,7 @@ def main(
def _init(
name: str,
- template: constants.Templates.Kind | None = constants.Templates.Kind.BLANK,
+ template: str | None = None,
loglevel: constants.LogLevel = config.loglevel,
):
"""Initialize a new Reflex app in the given directory."""
@@ -79,31 +79,20 @@ def _init(
app_name = prerequisites.validate_app_name(name)
console.rule(f"[bold]Initializing {app_name}")
+ # Check prerequisites.
prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
-
prerequisites.initialize_reflex_user_directory()
-
prerequisites.ensure_reflex_installation_id()
# When upgrading to 0.4, show migration instructions.
if prerequisites.should_show_rx_chakra_migration_instructions():
prerequisites.show_rx_chakra_migration_instructions()
- # Set up the app directory, only if the config doesn't exist.
- if not os.path.exists(constants.Config.FILE):
- if template is None:
- template = prerequisites.prompt_for_template()
- prerequisites.create_config(app_name)
- prerequisites.initialize_app_directory(app_name, template)
- telemetry_event = "init"
- else:
- telemetry_event = "reinit"
-
# Set up the web project.
prerequisites.initialize_frontend_dependencies()
- # Send the telemetry event after the .web folder is initialized.
- telemetry.send(telemetry_event)
+ # Initialize the app.
+ prerequisites.initialize_app(app_name, template)
# Migrate Pynecone projects to Reflex.
prerequisites.migrate_to_reflex()
@@ -123,7 +112,7 @@ def init(
name: str = typer.Option(
None, metavar="APP_NAME", help="The name of the app to initialize."
),
- template: constants.Templates.Kind = typer.Option(
+ template: str = typer.Option(
None,
help="The template to initialize the app with.",
),
@@ -576,20 +565,6 @@ def demo(
# Open the demo app in a terminal.
webbrowser.open("https://demo.reflex.run")
- # Later: open the demo app locally.
- # with tempfile.TemporaryDirectory() as tmp_dir:
- # os.chdir(tmp_dir)
- # _init(
- # name="reflex_demo",
- # template=constants.Templates.Kind.DEMO,
- # loglevel=constants.LogLevel.DEBUG,
- # )
- # _run(
- # frontend_port=frontend_port,
- # backend_port=backend_port,
- # loglevel=constants.LogLevel.DEBUG,
- # )
-
cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
diff --git a/reflex/testing.py b/reflex/testing.py
index fcaeefdff..9a90a5c6b 100644
--- a/reflex/testing.py
+++ b/reflex/testing.py
@@ -223,7 +223,7 @@ class AppHarness:
with chdir(self.app_path):
reflex.reflex._init(
name=self.app_name,
- template=reflex.constants.Templates.Kind.BLANK,
+ template=reflex.constants.Templates.DEFAULT,
loglevel=reflex.constants.LogLevel.INFO,
)
self.app_module_path.write_text(source_code)
diff --git a/reflex/utils/console.py b/reflex/utils/console.py
index ef37a2aa5..3dfff2828 100644
--- a/reflex/utils/console.py
+++ b/reflex/utils/console.py
@@ -2,8 +2,6 @@
from __future__ import annotations
-from typing import List, Optional
-
from rich.console import Console
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
from rich.prompt import Prompt
@@ -150,7 +148,10 @@ def error(msg: str, **kwargs):
def ask(
- question: str, choices: Optional[List[str]] = None, default: Optional[str] = None
+ question: str,
+ choices: list[str] | None = None,
+ default: str | None = None,
+ show_choices: bool = True,
) -> str:
"""Takes a prompt question and optionally a list of choices
and returns the user input.
@@ -159,11 +160,12 @@ def ask(
question: The question to ask the user.
choices: A list of choices to select from.
default: The default option selected.
+ show_choices: Whether to show the choices.
Returns:
A string with the user input.
"""
- return Prompt.ask(question, choices=choices, default=default) # type: ignore
+ return Prompt.ask(question, choices=choices, default=default, show_choices=show_choices) # type: ignore
def progress():
diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py
index 0d73ea247..ac445bf08 100644
--- a/reflex/utils/prerequisites.py
+++ b/reflex/utils/prerequisites.py
@@ -10,6 +10,7 @@ import os
import platform
import random
import re
+import shutil
import stat
import sys
import tempfile
@@ -30,6 +31,7 @@ from redis.asyncio import Redis
import reflex
from reflex import constants, model
+from reflex.base import Base
from reflex.compiler import templates
from reflex.config import Config, get_config
from reflex.utils import console, path_ops, processes
@@ -37,6 +39,15 @@ from reflex.utils import console, path_ops, processes
CURRENTLY_INSTALLING_NODE = False
+class Template(Base):
+ """A template for a Reflex app."""
+
+ name: str
+ description: str
+ code_url: str
+ demo_url: str
+
+
def check_latest_package_version(package_name: str):
"""Check if the latest version of the package is installed.
@@ -407,17 +418,42 @@ def initialize_requirements_txt():
console.info(f"Unable to check {fp} for reflex dependency.")
-def initialize_app_directory(app_name: str, template: constants.Templates.Kind):
+def initialize_app_directory(
+ app_name: str,
+ template_name: str = constants.Templates.DEFAULT,
+ template_code_dir_name: str | None = None,
+ template_dir: Path | None = None,
+):
"""Initialize the app directory on reflex init.
Args:
app_name: The name of the app.
- template: The template to use.
+ template_name: The name of the template to use.
+ template_code_dir_name: The name of the code directory in the template.
+ template_dir: The directory of the template source files.
+
+ Raises:
+ Exit: If template_name, template_code_dir_name, template_dir combination is not supported.
"""
console.log("Initializing the app directory.")
- # Copy the template to the current directory.
- template_dir = Path(constants.Templates.Dirs.BASE, "apps", template.value)
+ # By default, use the blank template from local assets.
+ if template_name == constants.Templates.DEFAULT:
+ if template_code_dir_name is not None or template_dir is not None:
+ console.error(
+ f"Only {template_name=} should be provided, got {template_code_dir_name=}, {template_dir=}."
+ )
+ raise typer.Exit(1)
+ template_code_dir_name = constants.Templates.Dirs.CODE
+ template_dir = Path(constants.Templates.Dirs.BASE, "apps", template_name)
+ else:
+ if template_code_dir_name is None or template_dir is None:
+ console.error(
+ f"For `{template_name}` template, `template_code_dir_name` and `template_dir` should both be provided."
+ )
+ raise typer.Exit(1)
+
+ console.debug(f"Using {template_name=} {template_dir=} {template_code_dir_name=}.")
# Remove all pyc and __pycache__ dirs in template directory.
for pyc_file in template_dir.glob("**/*.pyc"):
@@ -430,16 +466,16 @@ def initialize_app_directory(app_name: str, template: constants.Templates.Kind):
path_ops.cp(str(file), file.name)
# Rename the template app to the app name.
- path_ops.mv(constants.Templates.Dirs.CODE, app_name)
+ path_ops.mv(template_code_dir_name, app_name)
path_ops.mv(
- os.path.join(app_name, template_dir.name + constants.Ext.PY),
+ os.path.join(app_name, template_name + constants.Ext.PY),
os.path.join(app_name, app_name + constants.Ext.PY),
)
# Fix up the imports.
path_ops.find_replace(
app_name,
- f"from {constants.Templates.Dirs.CODE}",
+ f"from {template_name}",
f"from {app_name}",
)
@@ -999,33 +1035,35 @@ def check_schema_up_to_date():
)
-def prompt_for_template() -> constants.Templates.Kind:
+def prompt_for_template(templates: list[Template]) -> str:
"""Prompt the user to specify a template.
+ Args:
+ templates: The templates to choose from.
+
Returns:
- The template the user selected.
+ The template name the user selects.
"""
- # Show the user the URLs of each temlate to preview.
+ # Show the user the URLs of each template to preview.
console.print("\nGet started with a template:")
- console.print("blank (https://blank-template.reflex.run) - A minimal template.")
- console.print(
- "sidebar (https://sidebar-template.reflex.run) - A template with a sidebar to navigate pages."
- )
- console.print("")
# Prompt the user to select a template.
+ id_to_name = {
+ str(idx): f"{template.name} ({template.demo_url}) - {template.description}"
+ for idx, template in enumerate(templates)
+ }
+ for id in range(len(id_to_name)):
+ console.print(f"({id}) {id_to_name[str(id)]}")
+
template = console.ask(
"Which template would you like to use?",
- choices=[
- template.value
- for template in constants.Templates.Kind
- if template.value != "demo"
- ],
- default=constants.Templates.Kind.BLANK.value,
+ choices=[str(i) for i in range(len(id_to_name))],
+ show_choices=False,
+ default="0",
)
# Return the template.
- return constants.Templates.Kind(template)
+ return templates[int(template)].name
def should_show_rx_chakra_migration_instructions() -> bool:
@@ -1178,3 +1216,166 @@ def migrate_to_reflex():
for old, new in updates.items():
line = line.replace(old, new)
print(line, end="")
+
+
+def fetch_app_templates() -> dict[str, Template]:
+ """Fetch the list of app templates from the Reflex backend server.
+
+ Returns:
+ The name and download URL as a dictionary.
+ """
+ config = get_config()
+ if not config.cp_backend_url:
+ console.info(
+ "Skip fetching App templates. No backend URL is specified in the config."
+ )
+ return {}
+ try:
+ response = httpx.get(
+ f"{config.cp_backend_url}{constants.Templates.APP_TEMPLATES_ROUTE}"
+ )
+ response.raise_for_status()
+ return {
+ template["name"]: Template.parse_obj(template)
+ for template in response.json()
+ }
+ except httpx.HTTPError as ex:
+ console.info(f"Failed to fetch app templates: {ex}")
+ return {}
+ except (TypeError, KeyError, json.JSONDecodeError) as tkje:
+ console.info(f"Unable to process server response for app templates: {tkje}")
+ return {}
+
+
+def create_config_init_app_from_remote_template(
+ app_name: str,
+ template_url: str,
+):
+ """Create new rxconfig and initialize app using a remote template.
+
+ Args:
+ app_name: The name of the app.
+ template_url: The path to the template source code as a zip file.
+
+ Raises:
+ Exit: If any download, file operations fail or unexpected zip file format.
+
+ """
+ # Create a temp directory for the zip download.
+ try:
+ temp_dir = tempfile.mkdtemp()
+ except OSError as ose:
+ console.error(f"Failed to create temp directory for download: {ose}")
+ raise typer.Exit(1) from ose
+
+ # Use httpx GET with redirects to download the zip file.
+ zip_file_path = Path(temp_dir) / "template.zip"
+ try:
+ # Note: following redirects can be risky. We only allow this for reflex built templates at the moment.
+ response = httpx.get(template_url, follow_redirects=True)
+ console.debug(f"Server responded download request: {response}")
+ response.raise_for_status()
+ except httpx.HTTPError as he:
+ console.error(f"Failed to download the template: {he}")
+ raise typer.Exit(1) from he
+ try:
+ with open(zip_file_path, "wb") as f:
+ f.write(response.content)
+ console.debug(f"Downloaded the zip to {zip_file_path}")
+ except OSError as ose:
+ console.error(f"Unable to write the downloaded zip to disk {ose}")
+ raise typer.Exit(1) from ose
+
+ # Create a temp directory for the zip extraction.
+ try:
+ unzip_dir = Path(tempfile.mkdtemp())
+ except OSError as ose:
+ console.error(f"Failed to create temp directory for extracting zip: {ose}")
+ raise typer.Exit(1) from ose
+ try:
+ zipfile.ZipFile(zip_file_path).extractall(path=unzip_dir)
+ # The zip file downloaded from github looks like:
+ # repo-name-branch/**/*, so we need to remove the top level directory.
+ if len(subdirs := os.listdir(unzip_dir)) != 1:
+ console.error(f"Expected one directory in the zip, found {subdirs}")
+ raise typer.Exit(1)
+ template_dir = unzip_dir / subdirs[0]
+ console.debug(f"Template folder is located at {template_dir}")
+ except Exception as uze:
+ console.error(f"Failed to unzip the template: {uze}")
+ raise typer.Exit(1) from uze
+
+ # Move the rxconfig file here first.
+ path_ops.mv(str(template_dir / constants.Config.FILE), constants.Config.FILE)
+ new_config = get_config(reload=True)
+
+ # Get the template app's name from rxconfig in case it is different than
+ # the source code repo name on github.
+ template_name = new_config.app_name
+
+ create_config(app_name)
+ initialize_app_directory(
+ app_name,
+ template_name=template_name,
+ template_code_dir_name=template_name,
+ template_dir=template_dir,
+ )
+
+ # Clean up the temp directories.
+ shutil.rmtree(temp_dir)
+ shutil.rmtree(unzip_dir)
+
+
+def initialize_app(app_name: str, template: str | None = None):
+ """Initialize the app either from a remote template or a blank app. If the config file exists, it is considered as reinit.
+
+ Args:
+ app_name: The name of the app.
+ template: The name of the template to use.
+
+ Raises:
+ Exit: If template is directly provided in the command flag and is invalid.
+ """
+ # Local imports to avoid circular imports.
+ from reflex.utils import telemetry
+
+ # Check if the app is already initialized.
+ if os.path.exists(constants.Config.FILE):
+ telemetry.send("reinit")
+ return
+
+ # Get the available templates
+ templates: dict[str, Template] = fetch_app_templates()
+
+ # Prompt for a template if not provided.
+ if template is None and len(templates) > 0:
+ template = prompt_for_template(list(templates.values()))
+ elif template is None:
+ template = constants.Templates.DEFAULT
+ assert template is not None
+
+ # If the blank template is selected, create a blank app.
+ if template == constants.Templates.DEFAULT:
+ # Default app creation behavior: a blank app.
+ create_config(app_name)
+ initialize_app_directory(app_name)
+ else:
+ # Fetch App templates from the backend server.
+ console.debug(f"Available templates: {templates}")
+
+ # If user selects a template, it needs to exist.
+ if template in templates:
+ template_url = templates[template].code_url
+ else:
+ # Check if the template is a github repo.
+ if template.startswith("https://github.com"):
+ template_url = f"{template.strip('/')}/archive/main.zip"
+ else:
+ console.error(f"Template `{template}` not found.")
+ raise typer.Exit(1)
+ create_config_init_app_from_remote_template(
+ app_name=app_name,
+ template_url=template_url,
+ )
+
+ telemetry.send("init")