Incrementally Add New Packages (#1607)

This commit is contained in:
Alek Petuskey 2023-08-31 14:20:44 -07:00 committed by GitHub
parent 7d7b7901a9
commit fed75ea7f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 192 additions and 55 deletions

View File

@ -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"
}
}
}

View File

@ -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:

View File

@ -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

View File

@ -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})"

View File

@ -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"

View File

@ -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):

View File

@ -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

View File

@ -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"

View File

@ -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):

View File

@ -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]]

View File

@ -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"

View File

@ -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):

View File

@ -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"

View File

View File

@ -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)

View File

@ -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),

View File

@ -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):

View File

@ -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):

View File

@ -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).