Преглед изворни кода

Incrementally Add New Packages (#1607)

Alek Petuskey пре 1 година
родитељ
комит
fed75ea7f8

+ 2 - 18
reflex/.templates/web/package.json

@@ -7,7 +7,6 @@
     "prod": "next start"
     "prod": "next start"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@chakra-ui/icons": "^2.0.19",
     "@chakra-ui/react": "^2.6.0",
     "@chakra-ui/react": "^2.6.0",
     "@chakra-ui/system": "^2.5.6",
     "@chakra-ui/system": "^2.5.6",
     "@emotion/react": "^11.10.6",
     "@emotion/react": "^11.10.6",
@@ -15,32 +14,17 @@
     "axios": "^1.4.0",
     "axios": "^1.4.0",
     "chakra-react-select": "^4.6.0",
     "chakra-react-select": "^4.6.0",
     "focus-visible": "^5.2.0",
     "focus-visible": "^5.2.0",
-    "framer-motion": "^10.12.4",
-    "gridjs": "^6.0.6",
-    "gridjs-react": "^6.0.1",
     "json5": "^2.2.3",
     "json5": "^2.2.3",
     "next": "^13.3.1",
     "next": "^13.3.1",
     "next-sitemap": "^4.1.8",
     "next-sitemap": "^4.1.8",
-    "plotly.js": "^2.22.0",
     "react": "^18.2.0",
     "react": "^18.2.0",
-    "react-debounce-input": "^3.3.0",
     "react-dom": "^18.2.0",
     "react-dom": "^18.2.0",
-    "react-dropzone": "^14.2.3",
-    "react-markdown": "^8.0.7",
-    "react-player": "^2.12.0",
-    "react-plotly.js": "^2.6.0",
-    "react-syntax-highlighter": "^15.5.0",
-    "rehype-katex": "^6.0.3",
-    "rehype-raw": "^6.1.1",
-    "remark-gfm": "^3.0.1",
-    "remark-math": "^5.1.1",
     "socket.io-client": "^4.6.1",
     "socket.io-client": "^4.6.1",
-    "universal-cookie": "^4.0.4",
-    "victory": "^36.6.8"
+    "universal-cookie": "^4.0.4"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "autoprefixer": "^10.4.14",
     "autoprefixer": "^10.4.14",
     "postcss": "^8.4.24",
     "postcss": "^8.4.24",
     "tailwindcss": "^3.3.2"
     "tailwindcss": "^3.3.2"
   }
   }
-}
+}

+ 38 - 4
reflex/app.py

@@ -47,7 +47,7 @@ from reflex.route import (
     verify_route_validity,
     verify_route_validity,
 )
 )
 from reflex.state import DefaultState, State, StateManager, StateUpdate
 from reflex.state import DefaultState, State, StateManager, StateUpdate
-from reflex.utils import console, format, types
+from reflex.utils import console, format, prerequisites, types
 
 
 # Define custom types.
 # Define custom types.
 ComponentCallable = Callable[[], Component]
 ComponentCallable = Callable[[], Component]
@@ -483,6 +483,27 @@ class App(Base):
 
 
             admin.mount_to(self.api)
             admin.mount_to(self.api)
 
 
+    def get_frontend_packages(self, imports: Dict[str, str]):
+        """Gets the frontend packages to be installed and filters out the unnecessary ones.
+
+        Args:
+            imports: A dictionary containing the imports used in the current page.
+
+        Example:
+            >>> get_frontend_packages({"react": "16.14.0", "react-dom": "16.14.0"})
+        """
+        page_imports = [
+            i
+            for i in imports
+            if i not in compiler.DEFAULT_IMPORTS.keys()
+            and i != "focus-visible/dist/focus-visible"
+            and "next" not in i
+            and not i.startswith("/")
+            and i != ""
+        ]
+        page_imports.extend(get_config().frontend_packages)
+        prerequisites.install_frontend_packages(page_imports)
+
     def compile(self):
     def compile(self):
         """Compile the app and output it to the pages folder."""
         """Compile the app and output it to the pages folder."""
         if os.environ.get(constants.SKIP_COMPILE_ENV_VAR) == "yes":
         if os.environ.get(constants.SKIP_COMPILE_ENV_VAR) == "yes":
@@ -493,12 +514,13 @@ class App(Base):
             MofNCompleteColumn(),
             MofNCompleteColumn(),
             TimeElapsedColumn(),
             TimeElapsedColumn(),
         )
         )
-        task = progress.add_task("Compiling: ", total=len(self.pages))
 
 
-        # TODO: include all work done in progress indicator, not just self.pages
         for render, kwargs in DECORATED_PAGES:
         for render, kwargs in DECORATED_PAGES:
             self.add_page(render, **kwargs)
             self.add_page(render, **kwargs)
 
 
+        task = progress.add_task("Compiling: ", total=len(self.pages))
+        # TODO: include all work done in progress indicator, not just self.pages
+
         # Get the env mode.
         # Get the env mode.
         config = get_config()
         config = get_config()
 
 
@@ -509,6 +531,7 @@ class App(Base):
         custom_components = set()
         custom_components = set()
         # TODO Anecdotally, processes=2 works 10% faster (cpu_count=12)
         # TODO Anecdotally, processes=2 works 10% faster (cpu_count=12)
         thread_pool = ThreadPool()
         thread_pool = ThreadPool()
+        all_imports = {}
         with progress:
         with progress:
             for route, component in self.pages.items():
             for route, component in self.pages.items():
                 # TODO: this progress does not reflect actual threaded task completion
                 # TODO: this progress does not reflect actual threaded task completion
@@ -524,8 +547,12 @@ class App(Base):
                         ),
                         ),
                     )
                     )
                 )
                 )
+                # add component.get_imports() to all_imports
+                all_imports.update(component.get_imports())
+
                 # Add the custom components from the page to the set.
                 # Add the custom components from the page to the set.
                 custom_components |= component.get_custom_components()
                 custom_components |= component.get_custom_components()
+
         thread_pool.close()
         thread_pool.close()
         thread_pool.join()
         thread_pool.join()
 
 
@@ -537,7 +564,11 @@ class App(Base):
         # Compile the custom components.
         # Compile the custom components.
         compile_results.append(compiler.compile_components(custom_components))
         compile_results.append(compiler.compile_components(custom_components))
 
 
-        # Compile the root document with base styles and fonts.
+        # Iterate through all the custom components and add their imports to the all_imports
+        for component in custom_components:
+            all_imports.update(component.get_imports())
+
+        # Compile the root document with base styles and fonts
         compile_results.append(compiler.compile_document_root(self.stylesheets))
         compile_results.append(compiler.compile_document_root(self.stylesheets))
 
 
         # Compile the theme.
         # Compile the theme.
@@ -556,6 +587,9 @@ class App(Base):
         # Empty the .web pages directory
         # Empty the .web pages directory
         compiler.purge_web_pages_dir()
         compiler.purge_web_pages_dir()
 
 
+        # install frontend packages
+        self.get_frontend_packages(all_imports)
+
         # Write the pages at the end to trigger the NextJS hot reload only once.
         # Write the pages at the end to trigger the NextJS hot reload only once.
         thread_pool = ThreadPool()
         thread_pool = ThreadPool()
         for output_path, code in compile_results:
         for output_path, code in compile_results:

+ 15 - 2
reflex/compiler/utils.py

@@ -25,7 +25,7 @@ from reflex.components.component import Component, ComponentStyle, CustomCompone
 from reflex.state import Cookie, LocalStorage, State
 from reflex.state import Cookie, LocalStorage, State
 from reflex.style import Style
 from reflex.style import Style
 from reflex.utils import format, imports, path_ops
 from reflex.utils import format, imports, path_ops
-from reflex.vars import ImportVar
+from reflex.vars import ImportVar, NoRenderImportVar
 
 
 # To re-export this function.
 # To re-export this function.
 merge_imports = imports.merge_imports
 merge_imports = imports.merge_imports
@@ -42,6 +42,9 @@ def compile_import_statement(fields: Set[ImportVar]) -> Tuple[str, Set[str]]:
         default: default library. When install "import def from library".
         default: default library. When install "import def from library".
         rest: rest of libraries. When install "import {rest1, rest2} from library"
         rest: rest of libraries. When install "import {rest1, rest2} from library"
     """
     """
+    # ignore the NoRenderImportVar fields during compilation
+    fields = {field for field in fields if not isinstance(field, NoRenderImportVar)}
+
     # Check for default imports.
     # Check for default imports.
     defaults = {field for field in fields if field.is_default}
     defaults = {field for field in fields if field.is_default}
     assert len(defaults) < 2
     assert len(defaults) < 2
@@ -72,7 +75,8 @@ def validate_imports(imports: imports.ImportDict):
                 raise ValueError(
                 raise ValueError(
                     f"Can not compile, the tag {import_name} is used multiple time from {lib} and {used_tags[import_name]}"
                     f"Can not compile, the tag {import_name} is used multiple time from {lib} and {used_tags[import_name]}"
                 )
                 )
-            used_tags[import_name] = lib
+            if import_name is not None:
+                used_tags[import_name] = lib
 
 
 
 
 def compile_imports(imports: imports.ImportDict) -> List[dict]:
 def compile_imports(imports: imports.ImportDict) -> List[dict]:
@@ -87,6 +91,10 @@ def compile_imports(imports: imports.ImportDict) -> List[dict]:
     import_dicts = []
     import_dicts = []
     for lib, fields in imports.items():
     for lib, fields in imports.items():
         default, rest = compile_import_statement(fields)
         default, rest = compile_import_statement(fields)
+        # prevent all NoRenderImportVar from being rendered on the page
+        if any({f for f in fields if isinstance(f, NoRenderImportVar)}):  # type: ignore
+            continue
+
         if not lib:
         if not lib:
             assert not default, "No default field allowed for empty library."
             assert not default, "No default field allowed for empty library."
             assert rest is not None and len(rest) > 0, "No fields to import."
             assert rest is not None and len(rest) > 0, "No fields to import."
@@ -94,6 +102,11 @@ def compile_imports(imports: imports.ImportDict) -> List[dict]:
                 import_dicts.append(get_import_dict(module))
                 import_dicts.append(get_import_dict(module))
             continue
             continue
 
 
+        # remove the version before rendering the package imports
+        lib, at, version = lib.rpartition("@")
+        if not lib:
+            lib = at + version
+
         import_dicts.append(get_import_dict(lib, default, rest))
         import_dicts.append(get_import_dict(lib, default, rest))
     return import_dicts
     return import_dicts
 
 

+ 26 - 5
reflex/components/component.py

@@ -22,7 +22,7 @@ from reflex.event import (
 )
 )
 from reflex.style import Style
 from reflex.style import Style
 from reflex.utils import format, imports, types
 from reflex.utils import format, imports, types
-from reflex.vars import BaseVar, ImportVar, Var
+from reflex.vars import BaseVar, ImportVar, NoRenderImportVar, Var
 
 
 
 
 class Component(Base, ABC):
 class Component(Base, ABC):
@@ -40,6 +40,9 @@ class Component(Base, ABC):
     # The library that the component is based on.
     # The library that the component is based on.
     library: Optional[str] = None
     library: Optional[str] = None
 
 
+    # List here the non-react dependency needed by `library`
+    lib_dependencies: List[str] = []
+
     # The tag to use when rendering the component.
     # The tag to use when rendering the component.
     tag: Optional[str] = None
     tag: Optional[str] = None
 
 
@@ -515,9 +518,12 @@ class Component(Base, ABC):
         return code
         return code
 
 
     def _get_imports(self) -> imports.ImportDict:
     def _get_imports(self) -> imports.ImportDict:
+        imports = {}
         if self.library is not None and self.tag is not None:
         if self.library is not None and self.tag is not None:
-            return {self.library: {self.import_var}}
-        return {}
+            imports[self.library] = {self.import_var}
+        for dep in self.lib_dependencies:
+            imports[dep] = {NoRenderImportVar()}  # type: ignore
+        return imports
 
 
     def get_imports(self) -> imports.ImportDict:
     def get_imports(self) -> imports.ImportDict:
         """Get all the libraries and fields that are used by the component.
         """Get all the libraries and fields that are used by the component.
@@ -833,11 +839,26 @@ class NoSSRComponent(Component):
     """A dynamic component that is not rendered on the server."""
     """A dynamic component that is not rendered on the server."""
 
 
     def _get_imports(self):
     def _get_imports(self):
-        return {"next/dynamic": {ImportVar(tag="dynamic", is_default=True)}}
+        imports = {"next/dynamic": {ImportVar(tag="dynamic", is_default=True)}}
+
+        for dep in [self.library, *self.lib_dependencies]:
+            imports[dep] = {NoRenderImportVar()}  # type: ignore
+
+        return imports
 
 
     def _get_custom_code(self) -> str:
     def _get_custom_code(self) -> str:
         opts_fragment = ", { ssr: false });"
         opts_fragment = ", { ssr: false });"
-        library_import = f"const {self.tag} = dynamic(() => import('{self.library}')"
+
+        # extract the correct import name from library name
+        if self.library is None:
+            raise ValueError("Undefined library for NoSSRComponent")
+
+        import_name_parts = [p for p in self.library.rpartition("@") if p != ""]
+        import_name = (
+            import_name_parts[0] if import_name_parts[0] != "@" else self.library
+        )
+
+        library_import = f"const {self.tag} = dynamic(() => import('{import_name}')"
         mod_import = (
         mod_import = (
             # https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#with-named-exports
             # https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#with-named-exports
             f".then((mod) => mod.{self.tag})"
             f".then((mod) => mod.{self.tag})"

+ 1 - 1
reflex/components/datadisplay/code.py

@@ -19,7 +19,7 @@ PRISM_STYLES_PATH = "/styles/code/prism"
 class CodeBlock(Component):
 class CodeBlock(Component):
     """A code block."""
     """A code block."""
 
 
-    library = "react-syntax-highlighter"
+    library = "react-syntax-highlighter@^15.5.0"
 
 
     tag = "Prism"
     tag = "Prism"
 
 

+ 3 - 1
reflex/components/datadisplay/datatable.py

@@ -11,7 +11,9 @@ from reflex.vars import BaseVar, ComputedVar, ImportVar, Var
 class Gridjs(Component):
 class Gridjs(Component):
     """A component that wraps a nivo bar component."""
     """A component that wraps a nivo bar component."""
 
 
-    library = "gridjs-react"
+    library = "gridjs-react@^6.0.1"
+
+    lib_dependencies: List[str] = ["gridjs@^6.0.6"]
 
 
 
 
 class DataTable(Gridjs):
 class DataTable(Gridjs):

+ 1 - 1
reflex/components/forms/debounce.py

@@ -16,7 +16,7 @@ class DebounceInput(Component):
     is experiencing high latency.
     is experiencing high latency.
     """
     """
 
 
-    library = "react-debounce-input"
+    library = "react-debounce-input@^3.3.0"
     tag = "DebounceInput"
     tag = "DebounceInput"
 
 
     # Minimum input characters before triggering the on_change event
     # Minimum input characters before triggering the on_change event

+ 1 - 1
reflex/components/forms/upload.py

@@ -21,7 +21,7 @@ clear_selected_files = BaseVar(name="_e => setFiles((files) => [])", type_=Event
 class Upload(Component):
 class Upload(Component):
     """A file upload component."""
     """A file upload component."""
 
 
-    library = "react-dropzone"
+    library = "react-dropzone@^14.2.3"
 
 
     tag = "ReactDropzone"
     tag = "ReactDropzone"
 
 

+ 4 - 2
reflex/components/graphing/plotly.py

@@ -1,6 +1,6 @@
 """Component for displaying a plotly graph."""
 """Component for displaying a plotly graph."""
 
 
-from typing import Dict
+from typing import Dict, List
 
 
 from plotly.graph_objects import Figure
 from plotly.graph_objects import Figure
 
 
@@ -12,7 +12,9 @@ from reflex.vars import Var
 class PlotlyLib(NoSSRComponent):
 class PlotlyLib(NoSSRComponent):
     """A component that wraps a plotly lib."""
     """A component that wraps a plotly lib."""
 
 
-    library = "react-plotly.js"
+    library = "react-plotly.js@^2.6.0"
+
+    lib_dependencies: List[str] = ["plotly.js@^2.22.0"]
 
 
 
 
 class Plotly(PlotlyLib):
 class Plotly(PlotlyLib):

+ 1 - 1
reflex/components/graphing/victory.py

@@ -334,7 +334,7 @@ def data(graph: str, x: List, y: Optional[List] = None, **kwargs) -> List:
 class Victory(Component):
 class Victory(Component):
     """A component that wraps a victory lib."""
     """A component that wraps a victory lib."""
 
 
-    library = "victory"
+    library = "victory@^36.6.8"
 
 
     # The data to display.
     # The data to display.
     data: Var[List[Dict]]
     data: Var[List[Dict]]

+ 1 - 1
reflex/components/libs/react_player.py

@@ -11,7 +11,7 @@ class ReactPlayerComponent(NoSSRComponent):
     reference: https://github.com/cookpete/react-player.
     reference: https://github.com/cookpete/react-player.
     """
     """
 
 
-    library = "react-player/lazy"
+    library = "react-player@^2.12.0"
 
 
     tag = "ReactPlayer"
     tag = "ReactPlayer"
 
 

+ 1 - 1
reflex/components/media/icon.py

@@ -7,7 +7,7 @@ from reflex.utils import format
 class ChakraIconComponent(Component):
 class ChakraIconComponent(Component):
     """A component that wraps a Chakra icon component."""
     """A component that wraps a Chakra icon component."""
 
 
-    library = "@chakra-ui/icons"
+    library = "@chakra-ui/icons@^2.0.19"
 
 
 
 
 class Icon(ChakraIconComponent):
 class Icon(ChakraIconComponent):

+ 8 - 1
reflex/components/typography/markdown.py

@@ -32,7 +32,14 @@ components_by_tag: Dict[str, Callable] = {
 class Markdown(Component):
 class Markdown(Component):
     """A markdown component."""
     """A markdown component."""
 
 
-    library = "react-markdown"
+    library = "react-markdown@^8.0.7"
+
+    lib_dependencies: List[str] = [
+        "rehype-katex@^6.0.3",
+        "remark-math@^5.1.1",
+        "rehype-raw@^6.1.1",
+        "remark-gfm@^3.0.1",
+    ]
 
 
     tag = "ReactMarkdown"
     tag = "ReactMarkdown"
 
 

+ 0 - 0
reflex/py.typed


+ 3 - 0
reflex/reflex.py

@@ -127,6 +127,9 @@ def run(
         frontend = True
         frontend = True
         backend = True
         backend = True
 
 
+    if not frontend and backend:
+        _skip_compile()
+
     # Check that the app is initialized.
     # Check that the app is initialized.
     prerequisites.check_initialized(frontend=frontend)
     prerequisites.check_initialized(frontend=frontend)
 
 

+ 0 - 3
reflex/utils/build.py

@@ -223,9 +223,6 @@ def setup_frontend(
         root: The root path of the project.
         root: The root path of the project.
         disable_telemetry: Whether to disable the Next telemetry.
         disable_telemetry: Whether to disable the Next telemetry.
     """
     """
-    # Install frontend packages.
-    prerequisites.install_frontend_packages()
-
     # Copy asset files to public folder.
     # Copy asset files to public folder.
     path_ops.cp(
     path_ops.cp(
         src=str(root / constants.APP_ASSETS_DIR),
         src=str(root / constants.APP_ASSETS_DIR),

+ 69 - 8
reflex/utils/exec.py

@@ -2,11 +2,14 @@
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
+import hashlib
+import json
 import os
 import os
 import platform
 import platform
 import sys
 import sys
 from pathlib import Path
 from pathlib import Path
 
 
+import psutil
 import uvicorn
 import uvicorn
 
 
 from reflex import constants
 from reflex import constants
@@ -25,20 +28,78 @@ def start_watching_assets_folder(root):
     asset_watch.start()
     asset_watch.start()
 
 
 
 
+def detect_package_change(json_file_path: str) -> str:
+    """Calculates the SHA-256 hash of a JSON file and returns it as a hexadecimal string.
+
+    Args:
+        json_file_path (str): The path to the JSON file to be hashed.
+
+    Returns:
+        str: The SHA-256 hash of the JSON file as a hexadecimal string.
+
+    Example:
+        >>> detect_package_change("package.json")
+        'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2'
+    """
+    with open(json_file_path, "r") as file:
+        json_data = json.load(file)
+
+    # Calculate the hash
+    json_string = json.dumps(json_data, sort_keys=True)
+    hash_object = hashlib.sha256(json_string.encode())
+    return hash_object.hexdigest()
+
+
+def kill(proc_pid: int):
+    """Kills a process and all its child processes.
+
+    Args:
+        proc_pid (int): The process ID of the process to be killed.
+
+    Example:
+        >>> kill(1234)
+    """
+    process = psutil.Process(proc_pid)
+    for proc in process.children(recursive=True):
+        proc.kill()
+    process.kill()
+
+
 def run_process_and_launch_url(run_command: list[str]):
 def run_process_and_launch_url(run_command: list[str]):
     """Run the process and launch the URL.
     """Run the process and launch the URL.
 
 
     Args:
     Args:
         run_command: The command to run.
         run_command: The command to run.
     """
     """
-    process = processes.new_process(
-        run_command, cwd=constants.WEB_DIR, shell=constants.IS_WINDOWS
-    )
-
-    for line in processes.stream_logs("Starting frontend", process):
-        if "ready started server on" in line:
-            url = line.split("url: ")[-1].strip()
-            console.print(f"App running at: [bold green]{url}")
+    json_file_path = os.path.join(constants.WEB_DIR, "package.json")
+    last_hash = detect_package_change(json_file_path)
+    process = None
+    first_run = True
+
+    while True:
+        if process is None:
+            process = processes.new_process(
+                run_command, cwd=constants.WEB_DIR, shell=constants.IS_WINDOWS
+            )
+        if process.stdout:
+            for line in processes.stream_logs("Starting frontend", process):
+                if "ready started server on" in line:
+                    if first_run:
+                        url = line.split("url: ")[-1].strip()
+                        console.print(f"App running at: [bold green]{url}")
+                        first_run = False
+                    else:
+                        console.print(f"New packages detected updating app...")
+                else:
+                    console.debug(line)
+                    new_hash = detect_package_change(json_file_path)
+                    if new_hash != last_hash:
+                        last_hash = new_hash
+                        kill(process.pid)
+                        process = None
+                        break  # for line in process.stdout
+        if process is not None:
+            break  # while True
 
 
 
 
 def run_frontend(root: Path, port: str):
 def run_frontend(root: Path, port: str):

+ 13 - 6
reflex/utils/prerequisites.py

@@ -14,7 +14,7 @@ import zipfile
 from fileinput import FileInput
 from fileinput import FileInput
 from pathlib import Path
 from pathlib import Path
 from types import ModuleType
 from types import ModuleType
-from typing import Optional
+from typing import List, Optional
 
 
 import httpx
 import httpx
 import typer
 import typer
@@ -376,25 +376,32 @@ def install_bun():
     )
     )
 
 
 
 
-def install_frontend_packages():
-    """Installs the base and custom frontend packages."""
+def install_frontend_packages(packages: List[str]):
+    """Installs the base and custom frontend packages.
+
+    Args:
+        packages (List[str]): A list of package names to be installed.
+
+    Example:
+        >>> install_frontend_packages(["react", "react-dom"])
+    """
     # Install the base packages.
     # Install the base packages.
     process = processes.new_process(
     process = processes.new_process(
         [get_install_package_manager(), "install", "--loglevel", "silly"],
         [get_install_package_manager(), "install", "--loglevel", "silly"],
         cwd=constants.WEB_DIR,
         cwd=constants.WEB_DIR,
         shell=constants.IS_WINDOWS,
         shell=constants.IS_WINDOWS,
     )
     )
+
     processes.show_status("Installing base frontend packages", process)
     processes.show_status("Installing base frontend packages", process)
 
 
-    # Install the app packages.
-    packages = get_config().frontend_packages
+    # Install the custom packages, if any.
     if len(packages) > 0:
     if len(packages) > 0:
         process = processes.new_process(
         process = processes.new_process(
             [get_install_package_manager(), "add", *packages],
             [get_install_package_manager(), "add", *packages],
             cwd=constants.WEB_DIR,
             cwd=constants.WEB_DIR,
             shell=constants.IS_WINDOWS,
             shell=constants.IS_WINDOWS,
         )
         )
-        processes.show_status("Installing custom frontend packages", process)
+        processes.show_status("Installing frontend packages for components", process)
 
 
 
 
 def check_initialized(frontend: bool = True):
 def check_initialized(frontend: bool = True):

+ 6 - 0
reflex/vars.py

@@ -1372,6 +1372,12 @@ class ImportVar(Base):
         return hash((self.tag, self.is_default, self.alias))
         return hash((self.tag, self.is_default, self.alias))
 
 
 
 
+class NoRenderImportVar(ImportVar):
+    """A import that doesn't need to be rendered."""
+
+    ...
+
+
 def get_local_storage(key: Optional[Union[Var, str]] = None) -> BaseVar:
 def get_local_storage(key: Optional[Union[Var, str]] = None) -> BaseVar:
     """Provide a base var as payload to get local storage item(s).
     """Provide a base var as payload to get local storage item(s).