Incrementally Add New Packages (#1607)
This commit is contained in:
parent
7d7b7901a9
commit
fed75ea7f8
@ -7,7 +7,6 @@
|
||||
"prod": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.19",
|
||||
"@chakra-ui/react": "^2.6.0",
|
||||
"@chakra-ui/system": "^2.5.6",
|
||||
"@emotion/react": "^11.10.6",
|
||||
@ -15,32 +14,17 @@
|
||||
"axios": "^1.4.0",
|
||||
"chakra-react-select": "^4.6.0",
|
||||
"focus-visible": "^5.2.0",
|
||||
"framer-motion": "^10.12.4",
|
||||
"gridjs": "^6.0.6",
|
||||
"gridjs-react": "^6.0.1",
|
||||
"json5": "^2.2.3",
|
||||
"next": "^13.3.1",
|
||||
"next-sitemap": "^4.1.8",
|
||||
"plotly.js": "^2.22.0",
|
||||
"react": "^18.2.0",
|
||||
"react-debounce-input": "^3.3.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",
|
||||
"universal-cookie": "^4.0.4",
|
||||
"victory": "^36.6.8"
|
||||
"universal-cookie": "^4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.24",
|
||||
"tailwindcss": "^3.3.2"
|
||||
}
|
||||
}
|
||||
}
|
@ -47,7 +47,7 @@ from reflex.route import (
|
||||
verify_route_validity,
|
||||
)
|
||||
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.
|
||||
ComponentCallable = Callable[[], Component]
|
||||
@ -483,6 +483,27 @@ class App(Base):
|
||||
|
||||
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):
|
||||
"""Compile the app and output it to the pages folder."""
|
||||
if os.environ.get(constants.SKIP_COMPILE_ENV_VAR) == "yes":
|
||||
@ -493,12 +514,13 @@ class App(Base):
|
||||
MofNCompleteColumn(),
|
||||
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:
|
||||
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.
|
||||
config = get_config()
|
||||
|
||||
@ -509,6 +531,7 @@ class App(Base):
|
||||
custom_components = set()
|
||||
# TODO Anecdotally, processes=2 works 10% faster (cpu_count=12)
|
||||
thread_pool = ThreadPool()
|
||||
all_imports = {}
|
||||
with progress:
|
||||
for route, component in self.pages.items():
|
||||
# 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.
|
||||
custom_components |= component.get_custom_components()
|
||||
|
||||
thread_pool.close()
|
||||
thread_pool.join()
|
||||
|
||||
@ -537,7 +564,11 @@ class App(Base):
|
||||
# Compile the 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 the theme.
|
||||
@ -556,6 +587,9 @@ class App(Base):
|
||||
# Empty the .web pages directory
|
||||
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.
|
||||
thread_pool = ThreadPool()
|
||||
for output_path, code in compile_results:
|
||||
|
@ -25,7 +25,7 @@ from reflex.components.component import Component, ComponentStyle, CustomCompone
|
||||
from reflex.state import Cookie, LocalStorage, State
|
||||
from reflex.style import Style
|
||||
from reflex.utils import format, imports, path_ops
|
||||
from reflex.vars import ImportVar
|
||||
from reflex.vars import ImportVar, NoRenderImportVar
|
||||
|
||||
# To re-export this function.
|
||||
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".
|
||||
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.
|
||||
defaults = {field for field in fields if field.is_default}
|
||||
assert len(defaults) < 2
|
||||
@ -72,7 +75,8 @@ def validate_imports(imports: imports.ImportDict):
|
||||
raise ValueError(
|
||||
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]:
|
||||
@ -87,6 +91,10 @@ def compile_imports(imports: imports.ImportDict) -> List[dict]:
|
||||
import_dicts = []
|
||||
for lib, fields in imports.items():
|
||||
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:
|
||||
assert not default, "No default field allowed for empty library."
|
||||
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))
|
||||
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))
|
||||
return import_dicts
|
||||
|
||||
|
@ -22,7 +22,7 @@ from reflex.event import (
|
||||
)
|
||||
from reflex.style import Style
|
||||
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):
|
||||
@ -40,6 +40,9 @@ class Component(Base, ABC):
|
||||
# The library that the component is based on.
|
||||
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.
|
||||
tag: Optional[str] = None
|
||||
|
||||
@ -515,9 +518,12 @@ class Component(Base, ABC):
|
||||
return code
|
||||
|
||||
def _get_imports(self) -> imports.ImportDict:
|
||||
imports = {}
|
||||
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:
|
||||
"""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."""
|
||||
|
||||
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:
|
||||
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 = (
|
||||
# https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#with-named-exports
|
||||
f".then((mod) => mod.{self.tag})"
|
||||
|
@ -19,7 +19,7 @@ PRISM_STYLES_PATH = "/styles/code/prism"
|
||||
class CodeBlock(Component):
|
||||
"""A code block."""
|
||||
|
||||
library = "react-syntax-highlighter"
|
||||
library = "react-syntax-highlighter@^15.5.0"
|
||||
|
||||
tag = "Prism"
|
||||
|
||||
|
@ -11,7 +11,9 @@ from reflex.vars import BaseVar, ComputedVar, ImportVar, Var
|
||||
class Gridjs(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):
|
||||
|
@ -16,7 +16,7 @@ class DebounceInput(Component):
|
||||
is experiencing high latency.
|
||||
"""
|
||||
|
||||
library = "react-debounce-input"
|
||||
library = "react-debounce-input@^3.3.0"
|
||||
tag = "DebounceInput"
|
||||
|
||||
# Minimum input characters before triggering the on_change event
|
||||
|
@ -21,7 +21,7 @@ clear_selected_files = BaseVar(name="_e => setFiles((files) => [])", type_=Event
|
||||
class Upload(Component):
|
||||
"""A file upload component."""
|
||||
|
||||
library = "react-dropzone"
|
||||
library = "react-dropzone@^14.2.3"
|
||||
|
||||
tag = "ReactDropzone"
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Component for displaying a plotly graph."""
|
||||
|
||||
from typing import Dict
|
||||
from typing import Dict, List
|
||||
|
||||
from plotly.graph_objects import Figure
|
||||
|
||||
@ -12,7 +12,9 @@ from reflex.vars import Var
|
||||
class PlotlyLib(NoSSRComponent):
|
||||
"""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):
|
||||
|
@ -334,7 +334,7 @@ def data(graph: str, x: List, y: Optional[List] = None, **kwargs) -> List:
|
||||
class Victory(Component):
|
||||
"""A component that wraps a victory lib."""
|
||||
|
||||
library = "victory"
|
||||
library = "victory@^36.6.8"
|
||||
|
||||
# The data to display.
|
||||
data: Var[List[Dict]]
|
||||
|
@ -11,7 +11,7 @@ class ReactPlayerComponent(NoSSRComponent):
|
||||
reference: https://github.com/cookpete/react-player.
|
||||
"""
|
||||
|
||||
library = "react-player/lazy"
|
||||
library = "react-player@^2.12.0"
|
||||
|
||||
tag = "ReactPlayer"
|
||||
|
||||
|
@ -7,7 +7,7 @@ from reflex.utils import format
|
||||
class ChakraIconComponent(Component):
|
||||
"""A component that wraps a Chakra icon component."""
|
||||
|
||||
library = "@chakra-ui/icons"
|
||||
library = "@chakra-ui/icons@^2.0.19"
|
||||
|
||||
|
||||
class Icon(ChakraIconComponent):
|
||||
|
@ -32,7 +32,14 @@ components_by_tag: Dict[str, Callable] = {
|
||||
class 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"
|
||||
|
||||
|
@ -127,6 +127,9 @@ def run(
|
||||
frontend = True
|
||||
backend = True
|
||||
|
||||
if not frontend and backend:
|
||||
_skip_compile()
|
||||
|
||||
# Check that the app is initialized.
|
||||
prerequisites.check_initialized(frontend=frontend)
|
||||
|
||||
|
@ -223,9 +223,6 @@ def setup_frontend(
|
||||
root: The root path of the project.
|
||||
disable_telemetry: Whether to disable the Next telemetry.
|
||||
"""
|
||||
# Install frontend packages.
|
||||
prerequisites.install_frontend_packages()
|
||||
|
||||
# Copy asset files to public folder.
|
||||
path_ops.cp(
|
||||
src=str(root / constants.APP_ASSETS_DIR),
|
||||
|
@ -2,11 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import psutil
|
||||
import uvicorn
|
||||
|
||||
from reflex import constants
|
||||
@ -25,20 +28,78 @@ def start_watching_assets_folder(root):
|
||||
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]):
|
||||
"""Run the process and launch the URL.
|
||||
|
||||
Args:
|
||||
run_command: The command to run.
|
||||
"""
|
||||
process = processes.new_process(
|
||||
run_command, cwd=constants.WEB_DIR, shell=constants.IS_WINDOWS
|
||||
)
|
||||
json_file_path = os.path.join(constants.WEB_DIR, "package.json")
|
||||
last_hash = detect_package_change(json_file_path)
|
||||
process = None
|
||||
first_run = True
|
||||
|
||||
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}")
|
||||
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):
|
||||
|
@ -14,7 +14,7 @@ import zipfile
|
||||
from fileinput import FileInput
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
import httpx
|
||||
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.
|
||||
process = processes.new_process(
|
||||
[get_install_package_manager(), "install", "--loglevel", "silly"],
|
||||
cwd=constants.WEB_DIR,
|
||||
shell=constants.IS_WINDOWS,
|
||||
)
|
||||
|
||||
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:
|
||||
process = processes.new_process(
|
||||
[get_install_package_manager(), "add", *packages],
|
||||
cwd=constants.WEB_DIR,
|
||||
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):
|
||||
|
@ -1372,6 +1372,12 @@ class ImportVar(Base):
|
||||
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:
|
||||
"""Provide a base var as payload to get local storage item(s).
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user