From 29806b0c6aa0f37ceeb3ca66a0616ca577551cf4 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 29 Apr 2026 16:13:41 -0700 Subject: [PATCH] delete custom components --- docs/advanced_onboarding/code_structure.md | 4 - docs/advanced_onboarding/how-reflex-works.md | 2 +- docs/app/reflex_docs/pages/docs/__init__.py | 4 - .../pages/docs/custom_components.py | 424 ---------- .../pages/docs_landing/views/other.py | 5 +- .../templates/docpage/sidebar/sidebar.py | 21 - .../docpage/sidebar/sidebar_items/learn.py | 9 - docs/custom-components/command-reference.md | 154 ---- docs/custom-components/overview.md | 77 -- .../prerequisites-for-publishing.md | 54 -- docs/getting_started/basics.md | 2 +- docs/wrapping-react/overview.md | 2 - .../src/reflex_base/constants/__init__.py | 2 - .../constants/custom_components.py | 35 - reflex/constants/__init__.py | 2 - reflex/constants/custom_components.py | 4 - reflex/custom_components/__init__.py | 1 - reflex/custom_components/custom_components.py | 800 ------------------ reflex/reflex.py | 3 - 19 files changed, 5 insertions(+), 1600 deletions(-) delete mode 100644 docs/app/reflex_docs/pages/docs/custom_components.py delete mode 100644 docs/custom-components/command-reference.md delete mode 100644 docs/custom-components/overview.md delete mode 100644 docs/custom-components/prerequisites-for-publishing.md delete mode 100644 packages/reflex-base/src/reflex_base/constants/custom_components.py delete mode 100644 reflex/constants/custom_components.py delete mode 100644 reflex/custom_components/__init__.py delete mode 100644 reflex/custom_components/custom_components.py diff --git a/docs/advanced_onboarding/code_structure.md b/docs/advanced_onboarding/code_structure.md index f400bb498a7..027597f1c15 100644 --- a/docs/advanced_onboarding/code_structure.md +++ b/docs/advanced_onboarding/code_structure.md @@ -255,10 +255,6 @@ component. ### External Components -Reflex 0.4.3 introduced support for the [`reflex component` CLI commands](/docs/custom-components/overview), which makes it easy -to bundle up common functionality to publish on PyPI as a standalone Python package -that can be installed and used in any Reflex app. - When wrapping npm components or other self-contained bits of functionality, it can be helpful to move this complexity outside the app itself for easier maintenance and reuse in other apps. diff --git a/docs/advanced_onboarding/how-reflex-works.md b/docs/advanced_onboarding/how-reflex-works.md index 7e8a4ccc5e2..b188a164500 100644 --- a/docs/advanced_onboarding/how-reflex-works.md +++ b/docs/advanced_onboarding/how-reflex-works.md @@ -114,7 +114,7 @@ Many of our core components are based on [Radix](https://radix-ui.com/), a popul We chose React because it is a popular library with a huge ecosystem. Our goal isn't to recreate the web ecosystem, but to make it accessible to Python developers. -This also lets our users bring their own components if we don't have a component they need. Users can [wrap their own React components](/docs/wrapping-react/overview) and then [publish them](/docs/custom-components/overview) for others to use. Over time we will build out our [third party component ecosystem](/docs/custom-components/overview) so that users can easily find and use components that others have built. +This also lets our users bring their own components if we don't have a component they need. Users can [wrap their own React components](/docs/wrapping-react/overview) for use in their apps. ### Styling diff --git a/docs/app/reflex_docs/pages/docs/__init__.py b/docs/app/reflex_docs/pages/docs/__init__.py index 6ac16a0b29d..a1dd5389ed6 100644 --- a/docs/app/reflex_docs/pages/docs/__init__.py +++ b/docs/app/reflex_docs/pages/docs/__init__.py @@ -20,7 +20,6 @@ from .apiref import pages as apiref_pages from .cloud import pages as cloud_pages from .cloud_cliref import pages as cloud_cliref_pages -from .custom_components import custom_components from .library import library from .recipes_overview import overview @@ -135,8 +134,6 @@ def get_previews_from_frontmatter(filepath: str) -> dict[str, str]: manual_titles = { "docs/database/overview.md": "Database Overview", - "docs/custom-components/overview.md": "Custom Components Overview", - "docs/custom-components/command-reference.md": "Custom Component CLI Reference", "docs/api-routes/overview.md": "API Routes Overview", "docs/client_storage/overview.md": "Client Storage Overview", "docs/state_structure/overview.md": "State Structure Overview", @@ -266,7 +263,6 @@ def comp(_actual=actual_path, _virtual=virtual_doc): doc_routes = [ library, - custom_components, overview, *components_previews_pages, *apiref_pages, diff --git a/docs/app/reflex_docs/pages/docs/custom_components.py b/docs/app/reflex_docs/pages/docs/custom_components.py deleted file mode 100644 index a30b76702fd..00000000000 --- a/docs/app/reflex_docs/pages/docs/custom_components.py +++ /dev/null @@ -1,424 +0,0 @@ -import json -import os - -import httpx -import reflex as rx -from reflex_site_shared.components.icons import get_icon -from reflex_site_shared.styles.colors import c_color -from reflex_site_shared.styles.fonts import base -from reflex_site_shared.styles.shadows import shadows - -from reflex_docs.templates.docpage import docpage, h1_comp, text_comp_2 - -SORTING_CRITERIA = { - "Recent": lambda x: x["updated_at"], - "Downloads": lambda x: x["downloads"]["last_month"], -} - - -class CustomComponentGalleryState(rx.State): - tags: list[str] = [] - - components_list: list[dict[str, str]] = [] - paginated_data: list[dict[str, str]] = [] - - selected_filter: str = "" - original_components_list: list[dict[str, str]] = [] - - current_page: int = 1 - current_limit: int = 50 # Default number of items per page - total_pages: int = 1 - offset: int = 0 - number_of_rows: int = 0 - - # Added available limits for the number of items per page - limits: list[str] = ["10", "20", "50", "100"] - - @rx.event(background=True) - async def fetch_components_list(self): - try: - async with httpx.AsyncClient() as client: - response = await client.get( - f"{os.getenv('RCC_ENDPOINT')}/custom-components/gallery" - ) - response.raise_for_status() - component_list = response.json() - except (httpx.HTTPError, json.JSONDecodeError) as ex: - print(f"Internal error: failed to fetch components list due to: {ex}") - return - - for c in component_list: - c["downloads_last_month"] = c["downloads"]["last_month"] - c["keywords"] = [ - keyword - for keyword in c["keywords"] or [] - if "reflex" not in keyword.lower() - ] - c["download_url"] = package_url(c["package_name"]) - - async with self: - self.original_components_list = component_list - self.number_of_rows = len(component_list) - self.total_pages = ( - self.number_of_rows + self.current_limit - 1 - ) // self.current_limit - yield CustomComponentGalleryState.paginate() - - @rx.event - def paginate(self) -> None: - start = self.offset - end = start + self.current_limit - self.paginated_data = self.original_components_list[start:end] - self.current_page = (self.offset // self.current_limit) + 1 - - @rx.event - def delta_limit(self, limit: str) -> None: - self.current_limit = int(limit) - self.offset = 0 - self.total_pages = ( - self.number_of_rows + self.current_limit - 1 - ) // self.current_limit - self.paginate() - - @rx.event - def previous(self) -> None: - if self.offset >= self.current_limit: - self.offset -= self.current_limit - else: - self.offset = 0 - self.paginate() - - @rx.event - def next(self) -> None: - if self.offset + self.current_limit < self.number_of_rows: - self.offset += self.current_limit - self.paginate() - - @rx.event - def sort_components(self): - # Get the sorting function based on the selected filter - sorting_function = SORTING_CRITERIA.get( - self.selected_filter, lambda x: x["updated_at"] - ) - - # Both "Recent" and "Downloads" should be sorted in reverse order (newest/highest first) - if self.selected_filter in ["Recent", "Downloads"]: - self.original_components_list.sort(key=sorting_function, reverse=True) - else: - # Default sorting behavior, if no filter selected - self.original_components_list.sort(key=sorting_function, reverse=False) - - # After sorting, paginate the data - self.paginate() - - @rx.event - def set_selected_filter(self, filter_text: str): - # Reset to the first page when the filter is changed - self.selected_filter = filter_text - self.offset = 0 # Reset pagination - self.total_pages = ( - self.number_of_rows + self.current_limit - 1 - ) // self.current_limit # Recalculate total pages - self.sort_components() # Sort components based on selected filter - self.paginate() # Update paginated data - - -def filter_item( - icon: str, text: str, border: bool = False, on_click=None -) -> rx.Component: - is_selected = CustomComponentGalleryState.selected_filter == text - return rx.box( - get_icon(icon, class_name="py-[2px]", opacity=rx.cond(is_selected, 0.64, 1)), - rx.text(text, opacity=rx.cond(is_selected, 0.64, 1), class_name="font-small"), - rx.spacer(), - rx.cond( - is_selected, - rx.box( - class_name="size-2 justify-end bg-violet-9 rounded-full", - ), - ), - class_name="flex flex-row gap-[14px] items-center justify-start w-full cursor-pointer hover:bg-slate-3 transition-bg text-nowrap overflow-hidden p-[8px_14px]", - border_top=f"1px solid {c_color('slate', 5)}" if border else "none", - border_bottom=f"1px solid {c_color('slate', 5)}" if border else "none", - on_click=on_click, - ) - - -chips_box_style = { - "width": ["100%", "100%", "auto"], - "box-sizing": "border-box", - "display": "flex", - "flex-direction": "row", - "align_items": "center", - "padding": "6px 12px", - "cursor": "pointer", - "box-shadow": shadows["large"], - "border-radius": "1000px", - "transition": "background 0.075s ease-out, color 0.075s ease-out, border 0.075s ease-out", -} - -# Sorting -sorting_box_style = { - "gap": "12px", - "outline": "none", - "_focus": { - "outline": "none", - }, - **chips_box_style, -} - -menu_item_style = { - "box-sizing": "border-box", - "width": "191px", - "height": "auto", - "overflow": "hidden", - "padding": "0px", - "cursor": "default", - "background_color": c_color("slate", 2), - "border": f"1px solid {c_color('slate', 5)}", - "box-shadow": "0px 2px 4px rgba(0, 0, 0, 0.05)", - "border-radius": "12px", - "color": c_color("slate", 9), - **base, -} - - -def sorting_filters() -> rx.Component: - return rx.vstack( - filter_item( - "history", - "Recent", - on_click=lambda: CustomComponentGalleryState.set_selected_filter("Recent"), - ), - filter_item( - "arrow_down_big", - "Downloads", - border=True, - on_click=lambda: CustomComponentGalleryState.set_selected_filter( - "Downloads" - ), - ), - gap="0px", - width="100%", - ) - - -def sorting_filters_dropdown_menu() -> rx.Component: - condition = CustomComponentGalleryState.selected_filter != "" - conditional_style = { - "background": rx.cond( - condition, - c_color("violet", 9), - c_color("slate", 1), - ), - "color": rx.cond( - condition, - "white", - c_color("slate", 9), - ), - "border": rx.cond( - condition, - f"1px solid {c_color('violet', 9)}", - f"1px solid {c_color('slate', 5)}", - ), - "&[data-state='open']": { - "background": rx.cond( - condition, - c_color("violet", 9), - c_color("slate", 3), - ), - }, - "_hover": { - "background": rx.cond( - condition, - c_color("violet", 9), - c_color("slate", 3), - ), - }, - } - return rx.menu.root( - rx.menu.trigger( - rx.el.button( - rx.text( - "Sort", - rx.cond( - condition, - rx.text( - f": {CustomComponentGalleryState.selected_filter}", - as_="span", - class_name="text-nowrap", - ), - ), - as_="span", - class_name="font-small", - ), - get_icon( - icon="select", - ), - justify_content="space-between", - ), - style=sorting_box_style | conditional_style, - ), - rx.menu.content( - rx.menu.item(sorting_filters(), style=menu_item_style), - bg="transparent", - box_shadow="None", - padding="0px", - overflow="visible", - border="none", - align="center", - ), - width="100%", - ) - - -def package_url(package_name: str) -> str: - return f"https://pypi.org/pypi/{package_name}/" - - -def download(download_url: str) -> rx.Component: - return rx.link( - get_icon(icon="new_tab"), - underline="none", - href=download_url, - is_external=True, - class_name="text-slate-9 hover:!text-slate-9 bg-slate-1 hover:bg-slate-3 transition-bg cursor-pointer rounded-[6px]", - title="Documentation", - ) - - -def table_rows(category: dict): - name = rx.Var( - f"{category['package_name']!s}.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')", - ) - - updated_at = rx.Var( - f"({category['updated_at']}).split('T')[0].split('-').map((part, index) => index === 1 ? 'JanFebMarAprMayJunJulAugSepOctNovDec'.slice(part * 3, part * 3 + 3) : part.padStart(2, '0'))" - f".slice(1).join(' ') + ', ' + ({category['updated_at']}).split('T')[0].split('-')[0]" - ) - - return rx.table.row( - rx.table.cell(name), - rx.table.cell(updated_at), - rx.table.cell( - rx.box( - rx.text( - "pip install " + category["package_name"], - as_="p", - class_name="font-small truncate flex-1 min-w-0", - ), - get_icon(icon="copy", class_name="p-[5px]"), - on_click=rx.set_clipboard("pip install " + category["package_name"]), - class_name="flex flex-row gap-1.5 text-slate-9 w-full items-center overflow-hidden border border-slate-5 bg-slate-1 hover:bg-slate-3 transition-bg cursor-pointer shadow-small rounded-[6px] px-1.5 max-w-[20rem]", - ) - ), - rx.table.cell(download(category["download_url"])), - white_space="nowrap", - align="center", - ) - - -def component_grid(): - table = rx.table.root( - rx.table.header( - rx.table.row( - rx.foreach( - ["Package Name", "Last Updated", "Install Command", "Docs"], - lambda column_name: rx.table.column_header_cell( - rx.text(column_name, size="1"), - ), - ), - white_space="nowrap", - ), - ), - rx.table.body( - rx.foreach( - CustomComponentGalleryState.paginated_data, - table_rows, - ) - ), - width="100%", - variant="ghost", - max_width="800px", - size="1", - ) - - return rx.box( - table, - class_name="w-full h-full min-h-[60vh] flex flex-col items-start justify-start", - ) - - -def create_pagination(): - return rx.hstack( - rx.hstack( - rx.text("Rows per page", weight="bold", font_size="12px"), - rx.select( - CustomComponentGalleryState.limits, - default_value="50", - on_change=CustomComponentGalleryState.delta_limit, - width="80px", - ), - align_items="center", - ), - rx.hstack( - rx.text( - f"Page {CustomComponentGalleryState.current_page} of {CustomComponentGalleryState.total_pages}", - width="100px", - weight="bold", - font_size="12px", - ), - rx.button( - rx.icon( - tag="chevron-left", - on_click=CustomComponentGalleryState.previous, - size=25, - cursor="pointer", - ), - color_scheme="gray", - variant="surface", - size="1", - width="32px", - height="32px", - ), - rx.button( - rx.icon( - tag="chevron-right", - on_click=CustomComponentGalleryState.next, - size=25, - cursor="pointer", - ), - color_scheme="gray", - variant="surface", - size="1", - width="32px", - height="32px", - ), - align_items="center", - spacing="1", - ), - align_items="center", - spacing="4", - flex_wrap="wrap", - ) - - -@docpage(set_path="/custom-components/", right_sidebar=False) -def custom_components() -> rx.Component: - return rx.box( - rx.box( - h1_comp(text="Custom Components"), - rx.box( - text_comp_2( - text="Reflex has a growing ecosystem of custom components that you can use to build your apps. Below is a list of some of the custom components available for Reflex.", - ), - sorting_filters_dropdown_menu(), - class_name="flex flex-row w-full gap-12 justify-between items-center", - ), - class_name="flex flex-col w-full", - ), - component_grid(), - create_pagination(), - class_name="flex flex-col h-full w-full gap-6 mb-16", - on_mount=CustomComponentGalleryState.fetch_components_list, - ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/other.py b/docs/app/reflex_docs/pages/docs_landing/views/other.py index 3a9172779c5..a6455b92b93 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/other.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/other.py @@ -1,11 +1,12 @@ import reflex as rx from reflex_site_shared.constants import CONTRIBUTING_URL -from reflex_docs.pages.docs.custom_components import custom_components from reflex_docs.pages.docs_landing.views.link_item import faded_borders, link_item def other_section() -> rx.Component: + from reflex_docs.pages.docs import wrapping_react + return rx.el.section( rx.el.div( rx.el.h2( @@ -29,7 +30,7 @@ def other_section() -> rx.Component: "ReactIcon", "Extending with React Components", "See how to create and integrate your own React components into Reflex apps, allowing you to customize and extend your project’s capabilities.", - custom_components.path, + wrapping_react.overview.path, has_padding_left=True, ), faded_borders(), diff --git a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py index 8a86bf0d3fa..d5913a4b8da 100644 --- a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py +++ b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py @@ -433,7 +433,6 @@ def sidebar_comp( from reflex_docs.pages.docs import enterprise, getting_started, state, ui from reflex_docs.pages.docs import hosting as hosting_page from reflex_docs.pages.docs.apiref import pages - from reflex_docs.pages.docs.custom_components import custom_components from reflex_docs.pages.docs.library import library from reflex_docs.pages.docs.recipes_overview import overview @@ -667,26 +666,6 @@ def sidebar_comp( html_lib_index, url, ), - rx.link( # pyright: ignore [reportCallIssue] - rx.box( # pyright: ignore [reportCallIssue] - rx.box( # pyright: ignore [reportCallIssue] - rx.icon("atom", size=16), # pyright: ignore [reportCallIssue] - rx.el.h5( - "Custom Components", - class_name="font-smbold text-[0.875rem] text-slate-12 leading-5 tracking-[-0.01313rem] transition-color", - ), - class_name="flex flex-row items-center gap-3 text-slate-12", - ), - rx.text( # pyright: ignore [reportCallIssue] - "See what components people have made with Reflex!", - class_name="font-small text-slate-9", - ), - class_name="flex flex-col gap-2 border-slate-5 bg-slate-1 hover:bg-slate-3 shadow-large px-3.5 py-2 border rounded-xl transition-bg", - ), - underline="none", - href=custom_components.path, - class_name="w-fit lg:ml-[2.5rem]", - ), class_name="flex flex-col items-start gap-8 w-full list-none list-style-none", ), ), diff --git a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py index 0f4d652eb5d..820e6ddca77 100644 --- a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py +++ b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py @@ -40,7 +40,6 @@ def get_sidebar_items_frontend(): from reflex_docs.pages.docs import ( assets, components, - custom_components, library_, pages, styling, @@ -105,14 +104,6 @@ def get_sidebar_items_frontend(): wrapping_react.more_wrapping_examples, ], ), - create_item( - "Custom Components", - children=[ - custom_components.overview, - custom_components.prerequisites_for_publishing, - custom_components.command_reference, - ], - ), ] return items diff --git a/docs/custom-components/command-reference.md b/docs/custom-components/command-reference.md deleted file mode 100644 index 989312feb64..00000000000 --- a/docs/custom-components/command-reference.md +++ /dev/null @@ -1,154 +0,0 @@ - -# Command Reference - -The custom component commands are under `reflex component` subcommand. To see the list of available commands, run `reflex component --help`. To see the manual on a specific command, run `reflex component --help`, for example, `reflex component init --help`. - -```bash -reflex component --help -``` - -```text -Usage: reflex component [OPTIONS] COMMAND [ARGS]... - - Subcommands for creating and publishing Custom Components. - -Options: - --help Show this message and exit. - -Commands: - init Initialize a custom component. - build Build a custom component. - share Collect more details on the published package for gallery. -``` - -## reflex component init - -Below is an example of running the `init` command. - -```bash -reflex component init -``` - -```text -reflex component init -─────────────────────────────────────── Initializing reflex-google-auth project ─────────────────────────────────────── -Info: Populating pyproject.toml with package name: reflex-google-auth -Info: Initializing the component directory: custom_components/reflex_google_auth -Info: Creating app for testing: google_auth_demo -──────────────────────────────────────────── Initializing google_auth_demo ──────────────────────────────────────────── -[07:58:16] Initializing the app directory. console.py:85 - Initializing the web directory. console.py:85 -Success: Initialized google_auth_demo -─────────────────────────────────── Installing reflex-google-auth in editable mode. ─────────────────────────────────── -Info: Package reflex-google-auth installed! -Custom component initialized successfully! -─────────────────────────────────────────────────── Project Summary ─────────────────────────────────────────────────── -[ README.md ]: Package description. Please add usage examples. -[ pyproject.toml ]: Project configuration file. Please fill in details such as your name, email, homepage URL. -[ custom_components/ ]: Custom component code template. Start by editing it with your component implementation. -[ google_auth_demo/ ]: Demo App. Add more code to this app and test. -``` - -The `init` command uses the current enclosing folder name to construct a python package name, typically in the kebab case. For example, if running init in folder `google_auth`, the package name will be `reflex-google-auth`. The added prefix reduces the chance of name collision on PyPI (the Python Package Index), and it indicates that the package is a Reflex custom component. The user can override the package name by providing the `--package-name` option. - -The `init` command creates a set of files and folders prefilled with the package name and other details. During the init, the `custom_component` folder is installed locally in editable mode, so a developer can incrementally develop and test with ease. The changes in component implementation is automatically reflected where it is used. Below is the folder structure after the `init` command. - -```text -google_auth/ -├── pyproject.toml -├── README.md -├── custom_components/ -│ └── reflex_google_auth/ -│ ├── google_auth.py -│ └── __init__.py -└── google_auth_demo/ - └── assets/ - google_auth_demo/ - requirements.txt - rxconfig.py -``` - -### pyproject.toml - -The `pyproject.toml` is required for the package to build and be published. It is prefilled with information such as the package name, version (`0.0.1`), author name and email, homepage URL. By default the **Apache-2.0** license is used, the same as Reflex. If any of this information requires update, the user can edit the file by hand. - -### README - -The `README.md` file is created with installation instructions, e.g. `pip install reflex-google-auth`, and a brief description of the package. Typically the `README.md` contains usage examples. On PyPI, the `README.md` is rendered as part of the package page. - -### Custom Components Folder - -The `custom_components` folder is where the actual implementation is. Do not worry about this folder name: there is no need to change it. It is where `pyproject.toml` specifies the source of the python package is. The published package contains the contents inside it, excluding this folder. - -`reflex_google_auth` is the top folder for importable code. The `reflex_google_auth/__init__.py` imports everything from the `reflex_google_auth/google_auth.py`. For the user of the package, the import looks like `from reflex_google_auth import ABC, XYZ`. - -`reflex_google_auth/google_auth.py` is prefilled with code example and instructions from the [wrapping react guide](/docs/wrapping-react/overview). - -### Demo App Folder - -A demo app is generated inside `google_auth_demo` folder with import statements and example usage of the component. This is a regular Reflex app. Go into this directory and start using any reflex commands for testing. - -### Help Manual - -The help manual is shown when adding the `--help` option to the command. - -```bash -reflex component init --help -``` - -```text -Usage: reflex component init [OPTIONS] - - Initialize a custom component. - - Args: library_name: The name of the library. install: Whether to - install package from this local custom component in editable mode. - loglevel: The log level to use. - - Raises: Exit: If the pyproject.toml already exists. - -Options: - --library-name TEXT The name of your library. On PyPI, package - will be published as `reflex-{library- - name}`. - --install / --no-install Whether to install package from this local - custom component in editable mode. - [default: install] - --loglevel [debug|info|warning|error|critical] - The log level to use. [default: - LogLevel.INFO] - --help Show this message and exit. -``` - -## reflex component publish - -To publish to a package index, a user is required to already have an account with them. As of **0.7.5**, Reflex does not handle the publishing process for you. You can do so manually by first running `reflex component build` followed by `twine upload` or `uv publish` or your choice of a publishing utility. - -You can then share your build on our website with `reflex component share`. - -## reflex component build - -It is not required to run the `build` command separately before publishing. The `publish` command will build the package if it is not already built. The `build` command is provided for the user's convenience. - -The `build` command generates the `.tar.gz` and `.whl` distribution files to be uploaded to the desired package index, for example, PyPI. This command must be run at the top level of the project where the `pyproject.toml` file is. As a result of a successful build, there is a new `dist` folder with the distribution files. - -```bash -reflex component build --help -``` - -```text -Usage: reflex component build [OPTIONS] - - Build a custom component. Must be run from the project root directory where - the pyproject.toml is. - - Args: loglevel: The log level to use. - - Raises: Exit: If the build fails. - -Options: - --loglevel [debug|info|warning|error|critical] - The log level to use. [default: - LogLevel.INFO] - --help Show this message and exit. -``` diff --git a/docs/custom-components/overview.md b/docs/custom-components/overview.md deleted file mode 100644 index 3d0c36fa668..00000000000 --- a/docs/custom-components/overview.md +++ /dev/null @@ -1,77 +0,0 @@ -# Custom Components Overview - -```python exec -import reflex as rx -``` - -Reflex users create many components of their own: ready to use high level components, or nicely wrapped React components. With **Custom Components**, the community can easily share these components now. - -Release **0.4.3** introduces a series of `reflex component` commands that help developers wrap react components, test, and publish them as python packages. As shown in the image below, there are already a few custom components published on PyPI, such as `reflex-spline`, `reflex-webcam`. - -Check out the custom components gallery [here](/docs/custom-components/overview). - -```python eval -rx.center( - rx.image( - src="https://web.reflex-assets.dev/custom_components/pypi_reflex_custom_components.webp", - width="400px", - border_radius="15px", - border="1px solid", - ), -) -``` - -## Prerequisites for Publishing - -In order to publish a Python package, an account is required with a python package index, for example, PyPI. The documentation to create accounts and generate API tokens can be found on their websites. For a quick reference, check out our [Prerequisites for Publishing](/docs/custom-components/prerequisites-for-publishing) page. - -## Steps to Publishing - -Follow these steps to publish the custom component as a python package: - -1. `reflex component init`: creates a new custom component project from templates. -2. dev and test: developer implements and tests the custom component. -3. `reflex component build`: builds the package. -4. `twine upload` or `uv publish`: uploads the package to a python package index. - -### Initialization - -```bash -reflex component init -``` - -First create a new folder for your custom component project, for example `color_picker`. The package name will be `reflex-color-picker`. The prefix `reflex-` is intentionally added for all custom components for easy search on PyPI. If you prefer a particular name for the package, you can either change it manually in the `pyproject.toml` file or add the `--library-name` option in the `reflex component init` command initially. - -Run `reflex component init`, and a set of files and folders will be created in the `color_picker` folder. The `pyproject.toml` file is the configuration file for the project. The `custom_components` folder is where the custom component implementation is. The `color_picker_demo` folder is a demo Reflex app that uses the custom component. If this is the first time of creating python packages, it is encouraged to browse through all the files (there are not that many) to understand the structure of the project. - -```bash -color_picker/ -├── pyproject.toml <- Configuration file -├── README.md -├── .gitignore <- Exclude dist/ and metadata folders -├── custom_components/ -│ └── reflex_color_picker/ <- Custom component source directory -│ ├── color_picker.py -│ └── __init__.py -└── color_picker_demo/ <- Demo Reflex app directory - └── assets/ - color_picker_demo/ - requirements.txt - rxconfig.py -``` - -### Develop and Test - -After finishing the custom component implementation, the user is encouraged to fully test it before publishing. The generated Reflex demo app `color_picker_demo` is a good place to start. It is a regular Reflex app prefilled with imports and usage of this component. During the init, the `custom_component` folder is installed locally in editable mode, so a developer can incrementally develop and test with ease. The changes in component implementation are automatically reflected in the demo app. - -### Publish - -```bash -reflex component build -``` - -Once you're ready to publish your package, run `reflex component build` to build the package. The command builds the distribution files if they are not already built. The end result is a `dist` folder containing the distribution files. The user does not need to do anything manually with these distribution files. - -In order to publish these files as a Python package, you need to use a publishing utility. Any would work, but we recommend either [Twine](https://twine.readthedocs.io/en/stable/) or (uv)[https://docs.astral.sh/uv/guides/package/#publishing-your-package]. Make sure to keep your package version in pyproject.toml updated. - -You can also share your components with the rest of the community at our website using the command `reflex component share`. See you there! diff --git a/docs/custom-components/prerequisites-for-publishing.md b/docs/custom-components/prerequisites-for-publishing.md deleted file mode 100644 index 284f814814f..00000000000 --- a/docs/custom-components/prerequisites-for-publishing.md +++ /dev/null @@ -1,54 +0,0 @@ -# Python Package Index - -```python exec -import reflex as rx - -image_style = { - "width": "400px", - "border_radius": "12px", - "border": "1px solid var(--c-slate-5)", -} -``` - -In order to publish a Python package, you need to use a publishing utility. Any would work, but we recommend either [Twine](https://twine.readthedocs.io/en/stable/) or [uv](https://docs.astral.sh/uv/guides/package/#publishing-your-package). - -## PyPI - -It is straightforward to create accounts and API tokens with PyPI. There is official help on the [PyPI website](https://pypi.org/help/). For a quick reference here, go to the top right corner of the PyPI website and look for the button to register and fill out personal information. - -```python eval -rx.center( - rx.image( - src="https://web.reflex-assets.dev/custom_components/pypi_register.webp", - style=image_style, - margin_bottom="16px", - loading="lazy", - ), -) -``` - -A user can use username and password to authenticate with PyPI when publishing. - -```python eval -rx.center( - rx.image( - src="https://web.reflex-assets.dev/custom_components/pypi_account_settings.webp", - style=image_style, - margin_bottom="16px", - loading="lazy", - ), -) -``` - -Scroll down to the API tokens section and click on the "Add API token" button. Fill out the form and click "Generate API token". - -```python eval -rx.center( - rx.image( - src="https://web.reflex-assets.dev/custom_components/pypi_api_tokens.webp", - style=image_style, - width="700px", - loading="lazy", - ), -) -``` diff --git a/docs/getting_started/basics.md b/docs/getting_started/basics.md index d90d631b813..e5e1f5befe2 100644 --- a/docs/getting_started/basics.md +++ b/docs/getting_started/basics.md @@ -57,7 +57,7 @@ def my_div(): ) ``` -If you need a component not provided by Reflex, you can check the [3rd party ecosystem](/docs/custom-components) or [wrap your own React component](/docs/wrapping-react/library-and-tags). +If you need a component not provided by Reflex, you can [wrap your own React component](/docs/wrapping-react/library-and-tags). ## Customizing and styling components diff --git a/docs/wrapping-react/overview.md b/docs/wrapping-react/overview.md index 436eb257bd7..94b81e03d69 100644 --- a/docs/wrapping-react/overview.md +++ b/docs/wrapping-react/overview.md @@ -9,8 +9,6 @@ One of Reflex's most powerful features is the ability to wrap React components a If you want a specific component for your app but Reflex doesn't provide it, there's a good chance it's available as a React component. Search for it on [npm](https://www.npmjs.com/), and if it's there, you can use it in your Reflex app. You can also create your own local React components and wrap them in Reflex. -Once you wrap your component, you [publish it](/docs/custom-components/overview) to the Reflex library so that others can use it. - ## Simple Example Simple components that don't have any interaction can be wrapped with just a few lines of code. diff --git a/packages/reflex-base/src/reflex_base/constants/__init__.py b/packages/reflex-base/src/reflex_base/constants/__init__.py index e790572a13a..8bfd6430d50 100644 --- a/packages/reflex-base/src/reflex_base/constants/__init__.py +++ b/packages/reflex-base/src/reflex_base/constants/__init__.py @@ -45,7 +45,6 @@ PyprojectToml, RequirementsTxt, ) -from .custom_components import CustomComponents from .event import Endpoint, EventTriggers, SocketEvent from .installer import Bun, Node, PackageJson from .route import ( @@ -86,7 +85,6 @@ "CompileVars", "ComponentName", "Config", - "CustomComponents", "DefaultPage", "DefaultPorts", "Dirs", diff --git a/packages/reflex-base/src/reflex_base/constants/custom_components.py b/packages/reflex-base/src/reflex_base/constants/custom_components.py deleted file mode 100644 index a499327b19d..00000000000 --- a/packages/reflex-base/src/reflex_base/constants/custom_components.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Constants for the custom components.""" - -from __future__ import annotations - -from pathlib import Path -from types import SimpleNamespace - - -class CustomComponents(SimpleNamespace): - """Constants for the custom components.""" - - # The name of the custom components source directory. - SRC_DIR = Path("custom_components") - # The name of the custom components pyproject.toml file. - PYPROJECT_TOML = Path("pyproject.toml") - # The name of the custom components package README file. - PACKAGE_README = Path("README.md") - # The name of the custom components package .gitignore file. - PACKAGE_GITIGNORE = ".gitignore" - # The name of the distribution directory as result of a build. - DIST_DIR = "dist" - # The name of the init file. - INIT_FILE = "__init__.py" - # Suffixes for the distribution files. - DISTRIBUTION_FILE_SUFFIXES = [".tar.gz", ".whl"] - # The name to the URL of python package repositories. - REPO_URLS = { - # Note: the trailing slash is required for below URLs. - "pypi": "https://upload.pypi.org/legacy/", - "testpypi": "https://test.pypi.org/legacy/", - } - # The .gitignore file for the custom component project. - FILE = Path(".gitignore") - # Files to gitignore. - DEFAULTS = {"__pycache__/", "*.py[cod]", "*.egg-info/", "dist/"} diff --git a/reflex/constants/__init__.py b/reflex/constants/__init__.py index 69f79271d9e..448b133d37b 100644 --- a/reflex/constants/__init__.py +++ b/reflex/constants/__init__.py @@ -44,7 +44,6 @@ PyprojectToml, RequirementsTxt, ) -from .custom_components import CustomComponents from .event import Endpoint, EventTriggers, SocketEvent from .installer import Bun, Node, PackageJson from .route import ( @@ -85,7 +84,6 @@ "CompileVars", "ComponentName", "Config", - "CustomComponents", "DefaultPage", "DefaultPorts", "Dirs", diff --git a/reflex/constants/custom_components.py b/reflex/constants/custom_components.py deleted file mode 100644 index 03a7f79c998..00000000000 --- a/reflex/constants/custom_components.py +++ /dev/null @@ -1,4 +0,0 @@ -# pyright: reportWildcardImportFromLibrary=false -"""Re-export from reflex_base.constants.custom_components.""" - -from reflex_base.constants.custom_components import * # pragma: no cover diff --git a/reflex/custom_components/__init__.py b/reflex/custom_components/__init__.py deleted file mode 100644 index 5b2ab6a0cf4..00000000000 --- a/reflex/custom_components/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The Reflex custom components.""" diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py deleted file mode 100644 index 6b612a0628f..00000000000 --- a/reflex/custom_components/custom_components.py +++ /dev/null @@ -1,800 +0,0 @@ -"""CLI for creating custom components.""" - -from __future__ import annotations - -import os -import re -import subprocess -import sys -from collections import namedtuple -from contextlib import contextmanager -from pathlib import Path -from typing import Any - -import click -from reflex_base import constants -from reflex_base.constants import CustomComponents - -from reflex.utils import console, frontend_skeleton - - -def _pyproject_toml_template( - package_name: str, module_name: str, reflex_version: str -) -> str: - """Template for custom components pyproject.toml. - - Args: - package_name: The name of the package. - module_name: The name of the module. - reflex_version: The version of Reflex. - - Returns: - Rendered pyproject.toml content as string. - """ - return f"""[build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "{package_name}" -version = "0.0.1" -description = "Reflex custom component {module_name}" -readme = "README.md" -license = {{ text = "Apache-2.0" }} -requires-python = ">=3.10" -authors = [{{ name = "", email = "YOUREMAIL@domain.com" }}] -keywords = ["reflex","reflex-custom-components"] - -dependencies = ["reflex>={reflex_version}"] - -classifiers = ["Development Status :: 4 - Beta"] - -[project.urls] - -[project.optional-dependencies] -dev = ["build", "twine"] - -[tool.setuptools.packages.find] -where = ["custom_components"] -""" - - -def _readme_template(module_name: str, package_name: str) -> str: - """Template for custom components README. - - Args: - module_name: The name of the module. - package_name: The name of the package. - - Returns: - Rendered README.md content as string. - """ - return f"""# {module_name} - -A Reflex custom component {module_name}. - -## Installation - -```bash -pip install {package_name} -``` -""" - - -def _source_template(component_class_name: str, module_name: str) -> str: - """Template for custom components source. - - Args: - component_class_name: The name of the component class. - module_name: The name of the module. - - Returns: - Rendered custom component source code as string. - """ - return rf''' -"""Reflex custom component {component_class_name}.""" - -# For wrapping react guide, visit https://reflex.dev/docs/wrapping-react/overview/ - -import reflex as rx - -# Some libraries you want to wrap may require dynamic imports. -# This is because they they may not be compatible with Server-Side Rendering (SSR). -# To handle this in Reflex, all you need to do is subclass `NoSSRComponent` instead. -# For example: -# from reflex_base.components.component import NoSSRComponent -# class {component_class_name}(NoSSRComponent): -# pass - - -class {component_class_name}(rx.Component): - """{component_class_name} component.""" - - # The React library to wrap. - library = "Fill-Me" - - # The React component tag. - tag = "Fill-Me" - - # If the tag is the default export from the module, you must set is_default = True. - # This is normally used when components don't have curly braces around them when importing. - # is_default = True - - # If you are wrapping another components with the same tag as a component in your project - # you can use aliases to differentiate between them and avoid naming conflicts. - # alias = "Other{component_class_name}" - - # The props of the React component. - # Note: when Reflex compiles the component to Javascript, - # `snake_case` property names are automatically formatted as `camelCase`. - # The prop names may be defined in `camelCase` as well. - # some_prop: rx.Var[str] = "some default value" - # some_other_prop: rx.Var[int] = 1 - - # By default Reflex will install the library you have specified in the library property. - # However, sometimes you may need to install other libraries to use a component. - # In this case you can use the lib_dependencies property to specify other libraries to install. - # lib_dependencies: list[str] = [] - - # Event triggers declaration if any. - # Below is equivalent to merging `{{ "on_change": lambda e: [e] }}` - # onto the default event triggers of parent/base Component. - # The function defined for the `on_change` trigger maps event for the javascript - # trigger to what will be passed to the backend event handler function. - # on_change: rx.EventHandler[lambda e: [e]] - - # To add custom code to your component - # def _get_custom_code(self) -> str: - # return "const customCode = 'customCode';" - - -{module_name} = {component_class_name}.create -''' - - -def _init_template(module_name: str) -> str: - """Template for custom components __init__.py. - - Args: - module_name: The name of the module. - - Returns: - Rendered __init__.py content as string. - """ - return f"from .{module_name} import *" - - -def _demo_app_template(custom_component_module_dir: str, module_name: str) -> str: - """Template for custom components demo app. - - Args: - custom_component_module_dir: The directory of the custom component module. - module_name: The name of the module. - - Returns: - Rendered demo app source code as string. - """ - return rf''' -"""Welcome to Reflex! This file showcases the custom component in a basic app.""" - -from rxconfig import config - -import reflex as rx - -from {custom_component_module_dir} import {module_name} - -filename = f"{{config.app_name}}/{{config.app_name}}.py" - - -class State(rx.State): - """The app state.""" - pass - -def index() -> rx.Component: - return rx.center( - rx.theme_panel(), - rx.vstack( - rx.heading("Welcome to Reflex!", size="9"), - rx.text( - "Test your custom component by editing ", - rx.code(filename), - font_size="2em", - ), - {module_name}(), - align="center", - spacing="7", - ), - height="100vh", - ) - - -# Add state and page to the app. -app = rx.App() -app.add_page(index) -''' - - -def set_loglevel(ctx: Any, self: Any, value: str | None): - """Set the log level. - - Args: - ctx: The click context. - self: The click command. - value: The log level to set. - """ - if value is not None: - loglevel = constants.LogLevel.from_string(value) - console.set_log_level(loglevel) - - -@click.group -def custom_components_cli(): - """CLI for creating custom components.""" - - -loglevel_option = click.option( - "--loglevel", - type=click.Choice( - [loglevel.value for loglevel in constants.LogLevel], - case_sensitive=False, - ), - callback=set_loglevel, - is_eager=True, - expose_value=False, - help="The log level to use.", -) - -POST_CUSTOM_COMPONENTS_GALLERY_TIMEOUT = 15 - - -@contextmanager -def set_directory(working_directory: str | Path): - """Context manager that sets the working directory. - - Args: - working_directory: The working directory to change to. - - Yields: - Yield to the caller to perform operations in the working directory. - """ - current_directory = Path.cwd() - working_directory = Path(working_directory) - try: - os.chdir(working_directory) - yield - finally: - os.chdir(current_directory) - - -def _create_package_config(module_name: str, package_name: str): - """Create a package config pyproject.toml file. - - Args: - module_name: The name of the module. - package_name: The name of the package typically constructed with `reflex-` prefix and a meaningful library name. - """ - pyproject = Path(CustomComponents.PYPROJECT_TOML) - pyproject.write_text( - _pyproject_toml_template( - module_name=module_name, - package_name=package_name, - reflex_version=constants.Reflex.VERSION, - ) - ) - - -def _create_readme(module_name: str, package_name: str): - """Create a package README file. - - Args: - module_name: The name of the module. - package_name: The name of the python package to be published. - """ - readme = Path(CustomComponents.PACKAGE_README) - readme.write_text( - _readme_template( - module_name=module_name, - package_name=package_name, - ) - ) - - -def _write_source_and_init_py( - custom_component_src_dir: Path, - component_class_name: str, - module_name: str, -): - """Write the source code and init file from templates for the custom component. - - Args: - custom_component_src_dir: The name of the custom component source directory. - component_class_name: The name of the component class. - module_name: The name of the module. - """ - module_path = custom_component_src_dir / f"{module_name}.py" - module_path.write_text( - _source_template( - component_class_name=component_class_name, module_name=module_name - ) - ) - - init_path = custom_component_src_dir / CustomComponents.INIT_FILE - init_path.write_text(_init_template(module_name=module_name)) - - -def _populate_demo_app(name_variants: NameVariants): - """Populate the demo app that imports the custom components. - - Args: - name_variants: the tuple including various names such as package name, class name needed for the project. - """ - from reflex_base import constants - - from reflex.reflex import _init - - demo_app_dir = Path(name_variants.demo_app_dir) - demo_app_name = name_variants.demo_app_name - - console.info(f"Creating app for testing: {demo_app_dir!s}") - - demo_app_dir.mkdir(exist_ok=True) - - with set_directory(demo_app_dir): - # We start with the blank template as basis. - _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 template file. - demo_file = Path(f"{demo_app_name}/{demo_app_name}.py") - demo_file.write_text( - _demo_app_template( - custom_component_module_dir=name_variants.custom_component_module_dir, - module_name=name_variants.module_name, - ) - ) - # Append the custom component package to the requirements.txt file. - with Path(f"{constants.RequirementsTxt.FILE}").open(mode="a") as f: - f.write(f"{name_variants.package_name}\n") - - -def _get_default_library_name_parts() -> list[str]: - """Get the default library name. Based on the current directory name, remove any non-alphanumeric characters. - - Returns: - The parts of default library name. - - Raises: - SystemExit: If the current directory name is not suitable for python projects, and we cannot find a valid library name based off it. - """ - current_dir_name = Path.cwd().name - - cleaned_dir_name = re.sub(r"[^0-9a-zA-Z-_]+", "", current_dir_name).lower() - parts = [part for part in re.split(r"-|_", cleaned_dir_name) if part] - if parts and parts[0] == constants.Reflex.MODULE_NAME: - # If the directory name already starts with "reflex", remove it from the parts. - parts = parts[1:] - # If no parts left, cannot find a valid library name, exit. - if not parts: - # The folder likely has a name not suitable for python paths. - console.error( - f"Based on current directory name {current_dir_name}, the library name is {constants.Reflex.MODULE_NAME}. This package already exists. Please use --library-name to specify a different name." - ) - raise SystemExit(1) - if not parts: - # The folder likely has a name not suitable for python paths. - console.error( - f"Could not find a valid library name based on the current directory: got {current_dir_name}." - ) - raise SystemExit(1) - return parts - - -NameVariants = namedtuple( - "NameVariants", - [ - "library_name", - "component_class_name", - "package_name", - "module_name", - "custom_component_module_dir", - "demo_app_dir", - "demo_app_name", - ], -) - - -def _validate_library_name(library_name: str | None) -> NameVariants: - """Validate the library name. - - Args: - library_name: The name of the library if picked otherwise None. - - Returns: - A tuple containing the various names such as package name, class name, etc., needed for the project. - - Raises: - SystemExit: If the library name is not suitable for python projects. - """ - if library_name is not None and not re.match( - r"^[a-zA-Z-]+[a-zA-Z0-9-]*$", library_name - ): - console.error( - f"Please use only alphanumeric characters or dashes: got {library_name}" - ) - raise SystemExit(1) - - # If not specified, use the current directory name to form the module name. - name_parts = ( - [part.lower() for part in library_name.split("-")] - if library_name - else _get_default_library_name_parts() - ) - if not library_name: - library_name = "-".join(name_parts) - - # Component class name is the camel case. - component_class_name = "".join([part.capitalize() for part in name_parts]) - console.debug(f"Component class name: {component_class_name}") - - # Package name is commonly kebab case. - package_name = f"reflex-{library_name}" - console.debug(f"Package name: {package_name}") - - # Module name is the snake case. - module_name = "_".join(name_parts) - - custom_component_module_dir = Path(f"reflex_{module_name}") - console.debug(f"Custom component source directory: {custom_component_module_dir}") - - # Use the same name for the directory and the app. - demo_app_dir = demo_app_name = f"{module_name}_demo" - console.debug(f"Demo app directory: {demo_app_dir}") - - return NameVariants( - library_name=library_name, - component_class_name=component_class_name, - package_name=package_name, - module_name=module_name, - custom_component_module_dir=custom_component_module_dir, - demo_app_dir=demo_app_dir, - demo_app_name=demo_app_name, - ) - - -def _populate_custom_component_project(name_variants: NameVariants): - """Populate the custom component source directory. This includes the pyproject.toml, README.md, and the code template for the custom component. - - Args: - name_variants: the tuple including various names such as package name, class name needed for the project. - """ - console.info( - f"Populating pyproject.toml with package name: {name_variants.package_name}" - ) - # write pyproject.toml, README.md, etc. - _create_package_config( - module_name=name_variants.library_name, package_name=name_variants.package_name - ) - _create_readme( - module_name=name_variants.library_name, package_name=name_variants.package_name - ) - - console.info( - f"Initializing the component directory: {CustomComponents.SRC_DIR / name_variants.custom_component_module_dir}" - ) - CustomComponents.SRC_DIR.mkdir(exist_ok=True) - with set_directory(CustomComponents.SRC_DIR): - module_dir = Path(name_variants.custom_component_module_dir) - module_dir.mkdir(exist_ok=True, parents=True) - _write_source_and_init_py( - custom_component_src_dir=module_dir, - component_class_name=name_variants.component_class_name, - module_name=name_variants.module_name, - ) - - -@custom_components_cli.command(name="init") -@click.option( - "--library-name", - default=None, - help="The name of your library. On PyPI, package will be published as `reflex-{library-name}`.", -) -@click.option( - "--install/--no-install", - default=True, - help="Whether to install package from this local custom component in editable mode.", -) -@loglevel_option -def init( - library_name: str | None, - install: bool, -): - """Initialize a custom component. - - Args: - library_name: The name of the library. - install: Whether to install package from this local custom component in editable mode. - - Raises: - SystemExit: If the pyproject.toml already exists. - """ - from reflex.utils import exec - - if CustomComponents.PYPROJECT_TOML.exists(): - console.error(f"A {CustomComponents.PYPROJECT_TOML} already exists. Aborting.") - raise SystemExit(1) - - # Show system info. - exec.output_system_info() - - # Check the name follows the convention if picked. - name_variants = _validate_library_name(library_name) - - console.rule(f"[bold]Initializing {name_variants.package_name} project") - - _populate_custom_component_project(name_variants) - - _populate_demo_app(name_variants) - - # Initialize the .gitignore. - frontend_skeleton.initialize_gitignore( - gitignore_file=CustomComponents.FILE, files_to_ignore=CustomComponents.DEFAULTS - ) - - if install: - package_name = name_variants.package_name - console.rule(f"[bold]Installing {package_name} in editable mode.") - if _pip_install_on_demand(package_name=".", install_args=["-e"]): - console.info(f"Package {package_name} installed!") - else: - raise SystemExit(1) - - console.print("[bold]Custom component initialized successfully!") - console.rule("[bold]Project Summary") - console.print( - f"[ {CustomComponents.PACKAGE_README} ]: Package description. Please add usage examples." - ) - console.print( - f"[ {CustomComponents.PYPROJECT_TOML} ]: Project configuration file. Please fill in details such as your name, email, homepage URL." - ) - console.print( - f"[ {CustomComponents.SRC_DIR}/ ]: Custom component code template. Start by editing it with your component implementation." - ) - console.print( - f"[ {name_variants.demo_app_dir}/ ]: Demo App. Add more code to this app and test." - ) - - -def _pip_install_on_demand( - package_name: str, - install_args: list[str] | None = None, -) -> bool: - """Install a package on demand. - - Args: - package_name: The name of the package. - install_args: The additional arguments for the pip install command. - - Returns: - True if the package is installed successfully, False otherwise. - """ - install_args = install_args or [] - - install_cmds = [ - sys.executable, - "-m", - "pip", - "install", - *install_args, - package_name, - ] - console.debug(f"Install package: {' '.join(install_cmds)}") - return _run_commands_in_subprocess(install_cmds) - - -def _run_commands_in_subprocess(cmds: list[str]) -> bool: - """Run commands in a subprocess. - - Args: - cmds: The commands to run. - - Returns: - True if the command runs successfully, False otherwise. - """ - console.debug(f"Running command: {' '.join(cmds)}") - try: - result = subprocess.run(cmds, capture_output=True, text=True, check=True) - except subprocess.CalledProcessError as cpe: - console.error(cpe.stdout) - console.error(cpe.stderr) - return False - else: - console.debug(result.stdout) - return True - - -def _make_pyi_files(): - """Create pyi files for the custom component.""" - from reflex_base.utils.pyi_generator import PyiGenerator - - for top_level_dir in Path.cwd().iterdir(): - if not top_level_dir.is_dir() or top_level_dir.name.startswith("."): - continue - for dir, _, _ in top_level_dir.walk(): - if "__pycache__" in dir.name: - continue - PyiGenerator().scan_all([dir]) - - -def _run_build(): - """Run the build command. - - Raises: - SystemExit: If the build fails. - """ - console.print("Building custom component...") - - _make_pyi_files() - - cmds = [sys.executable, "-m", "build", "."] - if _run_commands_in_subprocess(cmds): - console.info("Custom component built successfully!") - else: - raise SystemExit(1) - - -@custom_components_cli.command(name="build") -@loglevel_option -def build(): - """Build a custom component. Must be run from the project root directory where the pyproject.toml is.""" - _run_build() - - -def _collect_details_for_gallery(): - """Helper to collect details on the custom component to be included in the gallery. - - Raises: - SystemExit: If pyproject.toml file is ill-formed or the request to the backend services fails. - """ - import httpx - from reflex_cli.utils import hosting - - console.rule("[bold]Authentication with Reflex Services") - console.print("First let's log in to Reflex backend services.") - access_token, _ = hosting.authenticated_token() - - if not access_token: - console.error( - "Unable to authenticate with Reflex backend services. Make sure you are logged in." - ) - raise SystemExit(1) - - console.rule("[bold]Custom Component Information") - params = {} - - package_name = console.ask("[ Published python package name ]") - console.print(f"[ Custom component package name ] : {package_name}") - params["package_name"] = package_name - - post_custom_components_gallery_endpoint = ( - "https://gallery-backend.reflex.dev/custom-components/gallery" - ) - - # Check the backend services if the user is allowed to update information of this package is already shared. - try: - console.debug( - f"Checking if user has permission to upsert information for {package_name} by POST." - ) - # Send a POST request to achieve two things at once: - # 1. Check if the package is already shared by the user. If not, the backend will return 403. - # 2. If this package is not shared before, this request records the package name in the backend. - response = httpx.post( - post_custom_components_gallery_endpoint, - headers={"Authorization": f"Bearer {access_token}"}, - data=params, - ) - if response.status_code == httpx.codes.FORBIDDEN: - console.error( - f"{package_name} is owned by another user. Unable to update the information for it." - ) - raise SystemExit(1) - response.raise_for_status() - except httpx.HTTPError as he: - console.error(f"Unable to complete request due to {he}.") - raise SystemExit(1) from None - - files = [] - if (image_file_and_extension := _get_file_from_prompt_in_loop()) is not None: - files.append(( - "files", - (image_file_and_extension[1], image_file_and_extension[0]), - )) - - demo_url = None - while True: - demo_url = ( - console.ask( - "[ Full URL of deployed demo app, e.g. `https://my-app.reflex.run` ] (enter to skip)" - ) - or None - ) - if _validate_url_with_protocol_prefix(demo_url): - break - if demo_url: - params["demo_url"] = demo_url - - # Now send the post request to Reflex backend services. - try: - console.debug(f"Sending custom component data: {params}") - response = httpx.post( - post_custom_components_gallery_endpoint, - headers={"Authorization": f"Bearer {access_token}"}, - data=params, - files=files, - timeout=POST_CUSTOM_COMPONENTS_GALLERY_TIMEOUT, - ) - response.raise_for_status() - - except httpx.HTTPError as he: - console.error(f"Unable to complete request due to {he}.") - raise SystemExit(1) from None - - console.info("Custom component information successfully shared!") - - -def _validate_url_with_protocol_prefix(url: str | None) -> bool: - """Validate the URL with protocol prefix. Empty string is acceptable. - - Args: - url: the URL string to check. - - Returns: - Whether the entered URL is acceptable. - """ - return not url or (url.startswith(("http://", "https://"))) - - -def _get_file_from_prompt_in_loop() -> tuple[bytes, str] | None: - image_file = file_extension = None - while image_file is None: - image_path_str = console.ask( - "Upload a preview image of your demo app (enter to skip)" - ) - if not image_path_str: - break - image_file_path = Path(image_path_str) - if not image_file_path: - break - if not image_file_path.exists(): - console.error(f"File {image_file_path} does not exist.") - continue - file_extension = image_file_path.suffix - try: - image_file = image_file_path.read_bytes() - except OSError as ose: - console.error(f"Unable to read the {file_extension} file due to {ose}") - raise SystemExit(1) from None - else: - return image_file, file_extension - - console.debug(f"File extension detected: {file_extension}") - return None - - -@custom_components_cli.command(name="share") -@loglevel_option -def share_more_detail(): - """Collect more details on the published package for gallery.""" - _collect_details_for_gallery() - - -@custom_components_cli.command(name="install") -@loglevel_option -def install(): - """Install package from this local custom component in editable mode. - - Raises: - SystemExit: If unable to install the current directory in editable mode. - """ - if _pip_install_on_demand(package_name=".", install_args=["-e"]): - console.info("Package installed successfully!") - else: - raise SystemExit(1) diff --git a/reflex/reflex.py b/reflex/reflex.py index bf8a66afecd..7633f1f700f 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -13,8 +13,6 @@ from reflex_base.utils import console from reflex_cli.v2.deployments import hosting_cli -from reflex.custom_components.custom_components import custom_components_cli - if TYPE_CHECKING: from reflex_base.constants.base import LITERAL_ENV from reflex_cli.constants.base import LogLevel as HostingLogLevel @@ -938,7 +936,6 @@ def _convert_reflex_loglevel_to_reflex_cli_loglevel( cli.add_command(hosting_cli_command, name="cloud") cli.add_command(db_cli, name="db") cli.add_command(script_cli, name="script") -cli.add_command(custom_components_cli, name="component") if __name__ == "__main__": cli()