895 lines
28 KiB
Python
895 lines
28 KiB
Python
"""Common utility functions used in the compiler."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import concurrent.futures
|
|
import copy
|
|
import operator
|
|
import traceback
|
|
from collections.abc import Mapping, Sequence
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any, TypedDict
|
|
from urllib.parse import urlparse
|
|
|
|
from reflex_base import constants
|
|
from reflex_base.components.component import Component, ComponentStyle, CustomComponent
|
|
from reflex_base.constants.state import CAMEL_CASE_MEMO_MARKER, FIELD_MARKER
|
|
from reflex_base.style import Style
|
|
from reflex_base.utils import format, imports
|
|
from reflex_base.utils.imports import ImportVar, ParsedImportDict
|
|
from reflex_base.vars.base import Field, Var, VarData
|
|
from reflex_base.vars.function import DestructuredArg
|
|
from reflex_components_core.base import Description, Image, Scripts
|
|
from reflex_components_core.base.document import Links, ScrollRestoration
|
|
from reflex_components_core.base.document import Meta as ReactMeta
|
|
from reflex_components_core.el.elements.metadata import Head, Link, Meta, Title
|
|
from reflex_components_core.el.elements.other import Html
|
|
from reflex_components_core.el.elements.sectioning import Body
|
|
|
|
from reflex.experimental.memo import (
|
|
ExperimentalMemoComponentDefinition,
|
|
ExperimentalMemoFunctionDefinition,
|
|
)
|
|
from reflex.istate.storage import Cookie, LocalStorage, SessionStorage
|
|
from reflex.state import BaseState, _resolve_delta
|
|
from reflex.utils import path_ops
|
|
from reflex.utils.prerequisites import get_web_dir
|
|
|
|
# To re-export this function.
|
|
merge_imports = imports.merge_imports
|
|
|
|
|
|
def compile_import_statement(fields: list[ImportVar]) -> tuple[str, list[str]]:
|
|
"""Compile an import statement.
|
|
|
|
Args:
|
|
fields: The set of fields to import from the library.
|
|
|
|
Returns:
|
|
The libraries for default and rest.
|
|
default: default library. When install "import def from library".
|
|
rest: rest of libraries. When install "import {rest1, rest2} from library"
|
|
|
|
Raises:
|
|
ValueError: If there is more than one default import.
|
|
"""
|
|
# ignore the ImportVar fields with render=False during compilation
|
|
fields_set = {field for field in fields if field.render}
|
|
|
|
# Check for default imports.
|
|
defaults = {field for field in fields_set if field.is_default}
|
|
if len(defaults) >= 2:
|
|
msg = "Only one default import is allowed."
|
|
raise ValueError(msg)
|
|
|
|
# Get the default import, and the specific imports.
|
|
default = next(iter({field.name for field in defaults}), "")
|
|
rest = {field.name for field in fields_set - defaults}
|
|
|
|
return default, sorted(rest)
|
|
|
|
|
|
def validate_imports(import_dict: ParsedImportDict):
|
|
"""Verify that the same Tag is not used in multiple import.
|
|
|
|
Args:
|
|
import_dict: The dict of imports to validate
|
|
|
|
Raises:
|
|
ValueError: if a conflict on "tag/alias" is detected for an import.
|
|
"""
|
|
used_tags = {}
|
|
for lib, imported_items in import_dict.items():
|
|
for imported_item in imported_items:
|
|
import_name = (
|
|
f"{imported_item.tag}/{imported_item.alias}"
|
|
if imported_item.alias
|
|
else imported_item.tag
|
|
)
|
|
if import_name in used_tags:
|
|
already_imported = used_tags[import_name]
|
|
if (already_imported[0] == "$" and already_imported[1:] == lib) or (
|
|
lib[0] == "$" and lib[1:] == already_imported
|
|
):
|
|
used_tags[import_name] = lib if lib[0] == "$" else already_imported
|
|
continue
|
|
msg = f"Can not compile, the tag {import_name} is used multiple time from {lib} and {used_tags[import_name]}"
|
|
raise ValueError(msg)
|
|
if import_name is not None:
|
|
used_tags[import_name] = lib
|
|
|
|
|
|
class _ImportDict(TypedDict):
|
|
lib: str
|
|
default: str
|
|
rest: list[str]
|
|
|
|
|
|
def compile_imports(import_dict: ParsedImportDict) -> list[_ImportDict]:
|
|
"""Compile an import dict.
|
|
|
|
Args:
|
|
import_dict: The import dict to compile.
|
|
|
|
Returns:
|
|
The list of import dict.
|
|
|
|
Raises:
|
|
ValueError: If an import in the dict is invalid.
|
|
"""
|
|
collapsed_import_dict: ParsedImportDict = imports.collapse_imports(import_dict)
|
|
validate_imports(collapsed_import_dict)
|
|
import_dicts: list[_ImportDict] = []
|
|
for lib, fields in collapsed_import_dict.items():
|
|
# prevent lib from being rendered on the page if all imports are non rendered kind
|
|
if not any(f.render for f in fields):
|
|
continue
|
|
|
|
lib_paths: dict[str, list[ImportVar]] = {}
|
|
|
|
for field in fields:
|
|
lib_paths.setdefault(field.package_path, []).append(field)
|
|
|
|
compiled = {
|
|
path: compile_import_statement(fields) for path, fields in lib_paths.items()
|
|
}
|
|
|
|
for path, (default, rest) in compiled.items():
|
|
if not lib:
|
|
if default:
|
|
msg = "No default field allowed for empty library."
|
|
raise ValueError(msg)
|
|
if rest is None or len(rest) == 0:
|
|
msg = "No fields to import."
|
|
raise ValueError(msg)
|
|
import_dicts.extend(get_import_dict(module) for module in sorted(rest))
|
|
continue
|
|
|
|
# remove the version before rendering the package imports
|
|
formatted_lib = format.format_library_name(lib) + (
|
|
path if path != "/" else ""
|
|
)
|
|
|
|
import_dicts.append(get_import_dict(formatted_lib, default, rest))
|
|
return import_dicts
|
|
|
|
|
|
def get_import_dict(
|
|
lib: str, default: str = "", rest: list[str] | None = None
|
|
) -> _ImportDict:
|
|
"""Get dictionary for import template.
|
|
|
|
Args:
|
|
lib: The importing react library.
|
|
default: The default module to import.
|
|
rest: The rest module to import.
|
|
|
|
Returns:
|
|
A dictionary for import template.
|
|
"""
|
|
return _ImportDict(
|
|
lib=lib,
|
|
default=default,
|
|
rest=rest or [],
|
|
)
|
|
|
|
|
|
def save_error(error: Exception) -> str:
|
|
"""Save the error to a file.
|
|
|
|
Args:
|
|
error: The error to save.
|
|
|
|
Returns:
|
|
The path of the saved error.
|
|
"""
|
|
timestamp = datetime.now().strftime("%Y-%m-%d__%H-%M-%S")
|
|
constants.Reflex.LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
log_path = constants.Reflex.LOGS_DIR / f"error_{timestamp}.log"
|
|
traceback.TracebackException.from_exception(error).print(file=log_path.open("w+"))
|
|
return str(log_path)
|
|
|
|
|
|
def _sorted_keys(d: Mapping[str, Any]) -> dict[str, Any]:
|
|
"""Sort the keys of a dictionary.
|
|
|
|
Args:
|
|
d: The dictionary to sort.
|
|
|
|
Returns:
|
|
A new dictionary with sorted keys.
|
|
"""
|
|
return dict(sorted(d.items(), key=operator.itemgetter(0)))
|
|
|
|
|
|
def compile_state(state: type[BaseState]) -> dict:
|
|
"""Compile the state of the app.
|
|
|
|
Args:
|
|
state: The app state object.
|
|
|
|
Returns:
|
|
A dictionary of the compiled state.
|
|
"""
|
|
initial_state = state(_reflex_internal_init=True).dict(initial=True)
|
|
try:
|
|
_ = asyncio.get_running_loop()
|
|
except RuntimeError:
|
|
pass
|
|
else:
|
|
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
resolved_initial_state = pool.submit(
|
|
asyncio.run, _resolve_delta(initial_state)
|
|
).result()
|
|
return _sorted_keys(resolved_initial_state)
|
|
|
|
# Normally the compile runs before any event loop starts, we asyncio.run is available for calling.
|
|
return _sorted_keys(asyncio.run(_resolve_delta(initial_state)))
|
|
|
|
|
|
def _compile_client_storage_field(
|
|
field: Field,
|
|
) -> (
|
|
tuple[
|
|
type[Cookie] | type[LocalStorage] | type[SessionStorage],
|
|
dict[str, Any],
|
|
]
|
|
| tuple[None, None]
|
|
):
|
|
"""Compile the given cookie, local_storage or session_storage field.
|
|
|
|
Args:
|
|
field: The possible cookie field to compile.
|
|
|
|
Returns:
|
|
A dictionary of the compiled cookie or None if the field is not cookie-like.
|
|
"""
|
|
for field_type in (Cookie, LocalStorage, SessionStorage):
|
|
if isinstance(field.default, field_type):
|
|
cs_obj = field.default
|
|
elif isinstance(field.type_, type) and issubclass(field.type_, field_type):
|
|
cs_obj = field.type_()
|
|
else:
|
|
continue
|
|
return field_type, cs_obj.options()
|
|
return None, None
|
|
|
|
|
|
def _compile_client_storage_recursive(
|
|
state: type[BaseState],
|
|
) -> tuple[
|
|
dict[str, dict[str, Any]], dict[str, dict[str, Any]], dict[str, dict[str, Any]]
|
|
]:
|
|
"""Compile the client-side storage for the given state recursively.
|
|
|
|
Args:
|
|
state: The app state object.
|
|
|
|
Returns:
|
|
A tuple of the compiled client-side storage info: (cookies, local_storage, session_storage).
|
|
"""
|
|
cookies: dict[str, dict[str, Any]] = {}
|
|
local_storage: dict[str, dict[str, Any]] = {}
|
|
session_storage: dict[str, dict[str, Any]] = {}
|
|
state_name = state.get_full_name()
|
|
for name, field in state.__fields__.items():
|
|
if name in state.inherited_vars:
|
|
# only include vars defined in this state
|
|
continue
|
|
state_key = f"{state_name}.{name}" + FIELD_MARKER
|
|
field_type, options = _compile_client_storage_field(field)
|
|
if field_type is None or options is None:
|
|
continue
|
|
if field_type is Cookie:
|
|
cookies[state_key] = options
|
|
elif field_type is LocalStorage:
|
|
local_storage[state_key] = options
|
|
elif field_type is SessionStorage:
|
|
session_storage[state_key] = options
|
|
else:
|
|
continue
|
|
for substate in state.get_substates():
|
|
(
|
|
substate_cookies,
|
|
substate_local_storage,
|
|
substate_session_storage,
|
|
) = _compile_client_storage_recursive(substate)
|
|
cookies.update(substate_cookies)
|
|
local_storage.update(substate_local_storage)
|
|
session_storage.update(substate_session_storage)
|
|
return cookies, local_storage, session_storage
|
|
|
|
|
|
def compile_client_storage(
|
|
state: type[BaseState],
|
|
) -> dict[str, dict[str, dict[str, Any]]]:
|
|
"""Compile the client-side storage for the given state.
|
|
|
|
Args:
|
|
state: The app state object.
|
|
|
|
Returns:
|
|
A dictionary of the compiled client-side storage info.
|
|
"""
|
|
cookies, local_storage, session_storage = _compile_client_storage_recursive(state)
|
|
return {
|
|
constants.COOKIES: cookies,
|
|
constants.LOCAL_STORAGE: local_storage,
|
|
constants.SESSION_STORAGE: session_storage,
|
|
}
|
|
|
|
|
|
def compile_custom_component(
|
|
component: CustomComponent,
|
|
) -> tuple[dict, ParsedImportDict]:
|
|
"""Compile a custom component.
|
|
|
|
Args:
|
|
component: The custom component to compile.
|
|
|
|
Returns:
|
|
A tuple of the compiled component and the imports required by the component.
|
|
"""
|
|
# Render the component.
|
|
render = component.get_component()
|
|
|
|
# Get the imports.
|
|
imports: ParsedImportDict = {}
|
|
for lib, fields in render._get_all_imports().items():
|
|
if lib != component.library:
|
|
imports[lib] = fields
|
|
continue
|
|
|
|
filtered_fields = [field for field in fields if field.tag != component.tag]
|
|
if filtered_fields:
|
|
imports[lib] = filtered_fields
|
|
|
|
imports.setdefault("@emotion/react", []).append(ImportVar("jsx"))
|
|
|
|
# Concatenate the props.
|
|
props = list(component.props)
|
|
|
|
# Compile the component.
|
|
return (
|
|
{
|
|
"name": component.tag,
|
|
"props": props,
|
|
"signature": DestructuredArg(
|
|
fields=tuple(f"{prop}:{prop}{CAMEL_CASE_MEMO_MARKER}" for prop in props)
|
|
).to_javascript(),
|
|
"render": render.render(),
|
|
"hooks": render._get_all_hooks(),
|
|
"custom_code": render._get_all_custom_code(),
|
|
"dynamic_imports": render._get_all_dynamic_imports(),
|
|
},
|
|
imports,
|
|
)
|
|
|
|
|
|
def _apply_component_style_for_compile(component: Component) -> Component:
|
|
"""Apply the app style to a compiled component tree.
|
|
|
|
Args:
|
|
component: The component tree.
|
|
|
|
Returns:
|
|
The styled component tree.
|
|
"""
|
|
component._add_style_recursive(_app_style())
|
|
return component
|
|
|
|
|
|
def _apply_root_style(component: Component) -> None:
|
|
"""Merge app-level style into ``component.style`` without recursing.
|
|
|
|
Used for passthrough memo bodies where descendants render (and are styled)
|
|
in the page scope — only the root's style needs merging here.
|
|
|
|
Args:
|
|
component: The root component to style in place.
|
|
"""
|
|
if type(component)._add_style != Component._add_style:
|
|
msg = "Do not override _add_style directly. Use add_style instead."
|
|
raise UserWarning(msg)
|
|
style = _app_style()
|
|
new_style = component._add_style()
|
|
style_vars = [new_style._var_data]
|
|
component_style = component._get_component_style(style)
|
|
if component_style:
|
|
new_style.update(component_style)
|
|
style_vars.append(component_style._var_data)
|
|
new_style.update(component.style)
|
|
style_vars.append(component.style._var_data)
|
|
new_style._var_data = VarData.merge(*style_vars)
|
|
component.style = new_style
|
|
|
|
|
|
def _app_style() -> ComponentStyle | Style:
|
|
"""Return the active app-level component style map, or an empty one.
|
|
|
|
Returns:
|
|
The app-level style map.
|
|
"""
|
|
try:
|
|
from reflex.utils.prerequisites import get_and_validate_app
|
|
|
|
return get_and_validate_app().app.style
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def compile_experimental_component_memo(
|
|
definition: ExperimentalMemoComponentDefinition,
|
|
) -> tuple[dict, ParsedImportDict]:
|
|
"""Compile an experimental memo component.
|
|
|
|
Args:
|
|
definition: The component memo definition.
|
|
|
|
Returns:
|
|
A tuple of the compiled component definition and its imports.
|
|
"""
|
|
hole_child = definition.passthrough_hole_child
|
|
if hole_child is not None:
|
|
# Passthrough memo: shallow-copy the root only — ``render.children``
|
|
# still aliases the user-authored descendants so root-level walkers
|
|
# (e.g. ``Form._get_form_refs``) can introspect the real subtree, but
|
|
# we skip the O(n) deepcopy + recursive style pass. Descendants are
|
|
# rendered AND styled in the page scope, not here, so only the root
|
|
# needs app-level style merged.
|
|
render = copy.copy(definition.component)
|
|
_apply_root_style(render)
|
|
|
|
hooks = _root_only_hooks(render)
|
|
custom_code = _root_only_custom_code(render)
|
|
dynamic_imports = _root_only_dynamic_imports(render)
|
|
# Strings returned by the root's ``add_hooks`` can reference symbols
|
|
# (``refs``, ``StateContexts``, etc.) that normally reach this module
|
|
# through descendants' ``_get_hooks_imports`` / ``_get_imports``. JS
|
|
# imports are side-effect-free and dedup cleanly, so pulling the
|
|
# whole subtree's imports here is safe even when some go unused.
|
|
# ``_get_all_imports`` is read-only on the descendants, so the shallow
|
|
# aliasing above is fine.
|
|
all_imports = render._get_all_imports()
|
|
|
|
# Swap children for JSX render: the memo body template emits a
|
|
# ``{children}`` hole in place of the real descendants.
|
|
render.children = [hole_child]
|
|
rendered = render.render()
|
|
else:
|
|
render = _apply_component_style_for_compile(copy.deepcopy(definition.component))
|
|
rendered = render.render()
|
|
hooks = render._get_all_hooks()
|
|
custom_code = render._get_all_custom_code()
|
|
dynamic_imports = render._get_all_dynamic_imports()
|
|
all_imports = render._get_all_imports()
|
|
|
|
# Each experimental memo now lives in ``web/utils/components/<name>.jsx``,
|
|
# so importing the ``$/utils/components`` index from this file is only
|
|
# circular when ``<name>`` itself appears in that index — i.e. a legacy
|
|
# ``@rx.memo`` wrapper file. For auto-memo wrappers around legacy custom
|
|
# components, the index import is legitimate and must be preserved.
|
|
self_module = f"$/{constants.Dirs.COMPONENTS_PATH}/{definition.export_name}"
|
|
imports: ParsedImportDict = {
|
|
lib: fields for lib, fields in all_imports.items() if lib != self_module
|
|
}
|
|
|
|
imports.setdefault("@emotion/react", []).append(ImportVar("jsx"))
|
|
|
|
signature_fields = [
|
|
f"{param.js_prop_name}:{param.placeholder_name}"
|
|
for param in definition.params
|
|
if not param.is_children and not param.is_rest
|
|
]
|
|
|
|
if any(param.is_children for param in definition.params):
|
|
signature_fields.insert(0, "children")
|
|
|
|
rest_param = next((param for param in definition.params if param.is_rest), None)
|
|
|
|
return (
|
|
{
|
|
"kind": "component",
|
|
"name": definition.export_name,
|
|
"signature": DestructuredArg(
|
|
fields=tuple(signature_fields),
|
|
rest=rest_param.placeholder_name if rest_param is not None else None,
|
|
).to_javascript(),
|
|
"render": rendered,
|
|
"hooks": hooks,
|
|
"custom_code": custom_code,
|
|
"dynamic_imports": dynamic_imports,
|
|
},
|
|
imports,
|
|
)
|
|
|
|
|
|
def _root_only_hooks(component: Component) -> dict[str, VarData | None]:
|
|
"""Return hooks contributed by ``component`` itself, not its subtree.
|
|
|
|
Used by the passthrough memo compile path where descendants render in the
|
|
page scope — only the wrapper's own hooks (internal + ``add_hooks`` +
|
|
explicit ``_get_hooks``) belong in the memo body.
|
|
|
|
Args:
|
|
component: The root component whose own hooks to collect.
|
|
|
|
Returns:
|
|
The root-level hook map, keyed by hook source string.
|
|
"""
|
|
code: dict[str, VarData | None] = {}
|
|
code.update(component._get_hooks_internal())
|
|
explicit = component._get_hooks()
|
|
if explicit is not None:
|
|
code[explicit] = None
|
|
code.update(component._get_added_hooks())
|
|
return code
|
|
|
|
|
|
def _root_only_custom_code(component: Component) -> dict[str, None]:
|
|
"""Return custom code contributed by ``component`` itself, not its subtree.
|
|
|
|
Args:
|
|
component: The root component whose own custom code to collect.
|
|
|
|
Returns:
|
|
The root-level custom code snippets.
|
|
"""
|
|
code: dict[str, None] = {}
|
|
own = component._get_custom_code()
|
|
if own is not None:
|
|
code[own] = None
|
|
for clz in component._iter_parent_classes_with_method("add_custom_code"):
|
|
for item in clz.add_custom_code(component):
|
|
code[item] = None
|
|
return code
|
|
|
|
|
|
def _root_only_dynamic_imports(component: Component) -> set[str]:
|
|
"""Return dynamic imports contributed by ``component`` itself.
|
|
|
|
Args:
|
|
component: The root component whose own dynamic imports to collect.
|
|
|
|
Returns:
|
|
The root-level dynamic imports.
|
|
"""
|
|
own = component._get_dynamic_imports()
|
|
return {own} if own else set()
|
|
|
|
|
|
def compile_experimental_function_memo(
|
|
definition: ExperimentalMemoFunctionDefinition,
|
|
) -> tuple[dict, ParsedImportDict]:
|
|
"""Compile an experimental memo function.
|
|
|
|
Args:
|
|
definition: The function memo definition.
|
|
|
|
Returns:
|
|
A tuple of the compiled function definition and its imports.
|
|
"""
|
|
imports: ParsedImportDict = {}
|
|
if var_data := definition.function._get_all_var_data():
|
|
# Per-file memo modules live at ``$/utils/components/<name>``; strip
|
|
# only a self-import to this function memo's own module.
|
|
self_module = f"$/{constants.Dirs.COMPONENTS_PATH}/{definition.python_name}"
|
|
imports = {
|
|
lib: list(fields)
|
|
for lib, fields in dict(var_data.imports).items()
|
|
if lib != self_module
|
|
}
|
|
|
|
return (
|
|
{
|
|
"kind": "function",
|
|
"name": definition.python_name,
|
|
"function": str(definition.function),
|
|
},
|
|
imports,
|
|
)
|
|
|
|
|
|
def create_document_root(
|
|
head_components: Sequence[Component] | None = None,
|
|
html_lang: str | None = None,
|
|
html_custom_attrs: dict[str, Var | Any] | None = None,
|
|
) -> Component:
|
|
"""Create the document root.
|
|
|
|
Args:
|
|
head_components: The components to add to the head.
|
|
html_lang: The language of the document, will be added to the html root element.
|
|
html_custom_attrs: custom attributes added to the html root element.
|
|
|
|
Returns:
|
|
The document root.
|
|
"""
|
|
from reflex.utils.misc import preload_color_theme
|
|
|
|
existing_meta_types = set()
|
|
|
|
for component in head_components or []:
|
|
if isinstance(component, Meta):
|
|
if component.char_set is not None: # pyright: ignore[reportAttributeAccessIssue]
|
|
existing_meta_types.add("char_set")
|
|
if (
|
|
(name := component.name) is not None # pyright: ignore[reportAttributeAccessIssue]
|
|
and name.equals(Var.create("viewport"))
|
|
):
|
|
existing_meta_types.add("viewport")
|
|
|
|
# Always include the framework meta and link tags.
|
|
always_head_components = [
|
|
ReactMeta.create(),
|
|
Link.create(
|
|
rel="stylesheet",
|
|
type="text/css",
|
|
href=Var(
|
|
"reflexGlobalStyles",
|
|
_var_data=VarData(
|
|
imports={
|
|
"$/styles/__reflex_global_styles.css?url": [
|
|
ImportVar(tag="reflexGlobalStyles", is_default=True)
|
|
]
|
|
}
|
|
),
|
|
),
|
|
),
|
|
Links.create(),
|
|
]
|
|
maybe_head_components = []
|
|
# Only include these if the user has not specified them.
|
|
if "char_set" not in existing_meta_types:
|
|
maybe_head_components.append(Meta.create(char_set="utf-8"))
|
|
if "viewport" not in existing_meta_types:
|
|
maybe_head_components.append(
|
|
Meta.create(name="viewport", content="width=device-width, initial-scale=1")
|
|
)
|
|
|
|
# Add theme preload script as the very first component to prevent FOUC
|
|
theme_preload_components = [preload_color_theme()]
|
|
|
|
head_components = [
|
|
*theme_preload_components,
|
|
*(head_components or []),
|
|
*maybe_head_components,
|
|
*always_head_components,
|
|
]
|
|
html_component = Html.create(
|
|
Head.create(*head_components),
|
|
Body.create(
|
|
Var("children"),
|
|
ScrollRestoration.create(),
|
|
Scripts.create(),
|
|
),
|
|
lang=html_lang or "en",
|
|
custom_attrs=html_custom_attrs or {},
|
|
)
|
|
hooks = html_component._get_all_hooks()
|
|
if hooks:
|
|
msg = "You cannot use stateful components or hooks in the document root. Check your head components."
|
|
raise ValueError(msg)
|
|
return html_component
|
|
|
|
|
|
def create_theme(style: ComponentStyle) -> dict:
|
|
"""Create the base style for the app.
|
|
|
|
Args:
|
|
style: The style dict for the app.
|
|
|
|
Returns:
|
|
The base style for the app.
|
|
"""
|
|
# Get the global style from the style dict.
|
|
style_rules = Style({k: v for k, v in style.items() if isinstance(k, str)})
|
|
|
|
root_style = {
|
|
# Root styles.
|
|
":root": Style({
|
|
f"*{k}": v for k, v in style_rules.items() if k.startswith(":")
|
|
}),
|
|
# Body styles.
|
|
"body": Style(
|
|
{k: v for k, v in style_rules.items() if not k.startswith(":")},
|
|
),
|
|
}
|
|
|
|
# Return the theme.
|
|
return {"styles": {"global": root_style}}
|
|
|
|
|
|
def _format_route_part(part: str) -> str:
|
|
if part.startswith("[") and part.endswith("]"):
|
|
if part.startswith(("[...", "[[...")):
|
|
return "$"
|
|
if part.startswith("[["):
|
|
return "($" + part.removeprefix("[[").removesuffix("]]") + ")"
|
|
# We don't add [] here since we are reusing them from the input
|
|
return "$" + part
|
|
return "[" + part + "]"
|
|
|
|
|
|
def _path_to_file_stem(path: str) -> str:
|
|
if path == "index":
|
|
return "_index"
|
|
path = path if path != "index" else "/"
|
|
name = ".".join([_format_route_part(part) for part in path.split("/")]).lstrip(".")
|
|
return name + "._index" if not name.endswith("$") else name
|
|
|
|
|
|
def get_page_path(path: str) -> str:
|
|
"""Get the path of the compiled JS file for the given page.
|
|
|
|
Args:
|
|
path: The path of the page.
|
|
|
|
Returns:
|
|
The path of the compiled JS file.
|
|
"""
|
|
return str(
|
|
get_web_dir()
|
|
/ constants.Dirs.PAGES
|
|
/ constants.Dirs.ROUTES
|
|
/ (_path_to_file_stem(path) + constants.Ext.JSX)
|
|
)
|
|
|
|
|
|
def get_theme_path() -> str:
|
|
"""Get the path of the base theme style.
|
|
|
|
Returns:
|
|
The path of the theme style.
|
|
"""
|
|
return str(
|
|
get_web_dir()
|
|
/ constants.Dirs.UTILS
|
|
/ (constants.PageNames.THEME + constants.Ext.JS)
|
|
)
|
|
|
|
|
|
def get_root_stylesheet_path() -> str:
|
|
"""Get the path of the app root file.
|
|
|
|
Returns:
|
|
The path of the app root file.
|
|
"""
|
|
return str(
|
|
get_web_dir()
|
|
/ constants.Dirs.STYLES
|
|
/ (constants.PageNames.STYLESHEET_ROOT + constants.Ext.CSS)
|
|
)
|
|
|
|
|
|
def get_context_path() -> str:
|
|
"""Get the path of the context / initial state file.
|
|
|
|
Returns:
|
|
The path of the context module.
|
|
"""
|
|
return str(get_web_dir() / (constants.Dirs.CONTEXTS_PATH + constants.Ext.JS))
|
|
|
|
|
|
def get_components_path() -> str:
|
|
"""Get the path of the compiled components.
|
|
|
|
Returns:
|
|
The path of the compiled components.
|
|
"""
|
|
return str(
|
|
get_web_dir()
|
|
/ constants.Dirs.UTILS
|
|
/ (constants.PageNames.COMPONENTS + constants.Ext.JSX),
|
|
)
|
|
|
|
|
|
def get_memo_components_dir() -> str:
|
|
"""Get the directory that holds per-memo module files.
|
|
|
|
Returns:
|
|
The directory used for per-memo ``.jsx`` modules re-exported by the
|
|
top-level components index.
|
|
"""
|
|
return str(
|
|
get_web_dir() / constants.Dirs.UTILS / constants.PageNames.COMPONENTS,
|
|
)
|
|
|
|
|
|
def add_meta(
|
|
page: Component,
|
|
title: str,
|
|
image: str,
|
|
meta: Sequence[Mapping[str, Any] | Component],
|
|
description: str | None = None,
|
|
) -> Component:
|
|
"""Add metadata to a page.
|
|
|
|
Args:
|
|
page: The component for the page.
|
|
title: The title of the page.
|
|
image: The image for the page.
|
|
meta: The metadata list.
|
|
description: The description of the page.
|
|
|
|
Returns:
|
|
The component with the metadata added.
|
|
"""
|
|
meta_tags = [
|
|
item if isinstance(item, Component) else Meta.create(**item) for item in meta
|
|
]
|
|
|
|
children: list[Any] = [Title.create(title)]
|
|
if description:
|
|
children.append(Description.create(content=description))
|
|
children.append(Image.create(content=image))
|
|
|
|
page.children.extend(children)
|
|
page.children.extend(meta_tags)
|
|
|
|
return page
|
|
|
|
|
|
def resolve_path_of_web_dir(path: str | Path) -> Path:
|
|
"""Get the path under the web directory.
|
|
|
|
Args:
|
|
path: The path to get. It can be a relative or absolute path.
|
|
|
|
Returns:
|
|
The path under the web directory.
|
|
"""
|
|
path = Path(path)
|
|
web_dir = get_web_dir()
|
|
if path.is_relative_to(web_dir):
|
|
return path.absolute()
|
|
return (web_dir / path).absolute()
|
|
|
|
|
|
def write_file(path: str | Path, code: str):
|
|
"""Write the given code to the given path.
|
|
|
|
Args:
|
|
path: The path to write the code to.
|
|
code: The code to write.
|
|
"""
|
|
path = Path(path)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
if path.exists() and path.read_text(encoding="utf-8") == code:
|
|
return
|
|
path.write_text(code, encoding="utf-8")
|
|
|
|
|
|
def empty_dir(path: str | Path, keep_files: list[str] | None = None):
|
|
"""Remove all files and folders in a directory except for the keep_files.
|
|
|
|
Args:
|
|
path: The path to the directory that will be emptied
|
|
keep_files: List of filenames or foldernames that will not be deleted.
|
|
"""
|
|
path = Path(path)
|
|
|
|
# If the directory does not exist, return.
|
|
if not path.exists():
|
|
return
|
|
|
|
# Remove all files and folders in the directory.
|
|
keep_files = keep_files or []
|
|
for element in path.iterdir():
|
|
if element.name not in keep_files:
|
|
path_ops.rm(element)
|
|
|
|
|
|
def is_valid_url(url: str) -> bool:
|
|
"""Check if a url is valid.
|
|
|
|
Args:
|
|
url: The Url to check.
|
|
|
|
Returns:
|
|
Whether url is valid.
|
|
"""
|
|
result = urlparse(url)
|
|
return all([result.scheme, result.netloc])
|