211 lines
7.6 KiB
Python
211 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
import functools
|
|
from collections.abc import Iterable
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Callable
|
|
|
|
from . import core
|
|
from .dataclasses import KWONLY_SLOTS
|
|
from .helpers import hash_file_path
|
|
from .vbuild import VBuild
|
|
from .version import __version__
|
|
|
|
if TYPE_CHECKING:
|
|
from .element import Element
|
|
|
|
|
|
@dataclass(**KWONLY_SLOTS)
|
|
class Component:
|
|
key: str
|
|
name: str
|
|
path: Path
|
|
|
|
@property
|
|
def tag(self) -> str:
|
|
"""The tag of the component."""
|
|
return f'nicegui-{self.name}'
|
|
|
|
|
|
@dataclass(**KWONLY_SLOTS)
|
|
class VueComponent(Component):
|
|
html: str
|
|
script: str
|
|
style: str
|
|
|
|
|
|
@dataclass(**KWONLY_SLOTS)
|
|
class JsComponent(Component):
|
|
pass
|
|
|
|
|
|
@dataclass(**KWONLY_SLOTS)
|
|
class Resource:
|
|
key: str
|
|
path: Path
|
|
|
|
|
|
@dataclass(**KWONLY_SLOTS)
|
|
class DynamicResource:
|
|
name: str
|
|
function: Callable
|
|
|
|
|
|
@dataclass(**KWONLY_SLOTS)
|
|
class Library:
|
|
key: str
|
|
name: str
|
|
path: Path
|
|
|
|
|
|
@dataclass(**KWONLY_SLOTS)
|
|
class EsmModule:
|
|
name: str
|
|
path: Path
|
|
|
|
|
|
vue_components: dict[str, VueComponent] = {}
|
|
js_components: dict[str, JsComponent] = {}
|
|
libraries: dict[str, Library] = {}
|
|
resources: dict[str, Resource] = {}
|
|
dynamic_resources: dict[str, DynamicResource] = {}
|
|
esm_modules: dict[str, EsmModule] = {}
|
|
|
|
|
|
def register_vue_component(path: Path, *, max_time: float | None) -> Component:
|
|
"""Register a .vue or .js Vue component.
|
|
|
|
Single-file components (.vue) are built right away
|
|
to delegate this "long" process to the bootstrap phase
|
|
and to avoid building the component on every single request.
|
|
"""
|
|
key = compute_key(path, max_time=max_time)
|
|
name = _get_name(path)
|
|
if path.suffix == '.vue':
|
|
if key in vue_components and vue_components[key].path == path:
|
|
return vue_components[key]
|
|
assert key not in vue_components, f'Duplicate VUE component {key}'
|
|
v = VBuild(path)
|
|
vue_components[key] = VueComponent(key=key, name=name, path=path, html=v.html, script=v.script, style=v.style)
|
|
return vue_components[key]
|
|
if path.suffix == '.js':
|
|
if key in js_components and js_components[key].path == path:
|
|
return js_components[key]
|
|
assert key not in js_components, f'Duplicate JS component {key}'
|
|
js_components[key] = JsComponent(key=key, name=name, path=path)
|
|
return js_components[key]
|
|
raise ValueError(f'Unsupported component type "{path.suffix}"')
|
|
|
|
|
|
def register_library(path: Path, *, max_time: float | None) -> Library:
|
|
"""Register a *.js library."""
|
|
key = compute_key(path, max_time=max_time)
|
|
name = _get_name(path)
|
|
if path.suffix in {'.js', '.mjs'}:
|
|
if key in libraries and libraries[key].path == path:
|
|
return libraries[key]
|
|
assert key not in libraries, f'Duplicate js library {key}'
|
|
libraries[key] = Library(key=key, name=name, path=path)
|
|
return libraries[key]
|
|
raise ValueError(f'Unsupported library type "{path.suffix}"')
|
|
|
|
|
|
def register_resource(path: Path, *, max_time: float | None) -> Resource:
|
|
"""Register a resource."""
|
|
key = compute_key(path, max_time=max_time)
|
|
if key in resources and resources[key].path == path:
|
|
return resources[key]
|
|
assert key not in resources, f'Duplicate resource {key}'
|
|
resources[key] = Resource(key=key, path=path)
|
|
return resources[key]
|
|
|
|
|
|
def register_dynamic_resource(name: str, function: Callable) -> DynamicResource:
|
|
"""Register a dynamic resource which returns the result of a function."""
|
|
dynamic_resources[name] = DynamicResource(name=name, function=function)
|
|
return dynamic_resources[name]
|
|
|
|
|
|
def register_esm(name: str, path: Path, *, max_time: float | None) -> None:
|
|
"""Register an ESM module."""
|
|
if any(name == esm_module.name for esm_module in esm_modules.values()):
|
|
raise ValueError(f'Duplicate ESM module name "{name}"')
|
|
esm_modules[compute_key(path, max_time=max_time)] = EsmModule(name=name, path=path)
|
|
|
|
|
|
@functools.cache
|
|
def compute_key(path: Path, *, max_time: float | None) -> str:
|
|
"""Compute a key for a given path using a hash function.
|
|
|
|
If the path is relative to the NiceGUI base directory, the key is computed from the relative path.
|
|
"""
|
|
NICEGUI_BASE = Path(__file__).parent
|
|
try:
|
|
rel_path = path.relative_to(NICEGUI_BASE)
|
|
except ValueError:
|
|
rel_path = path
|
|
if path.is_file():
|
|
return f'{hash_file_path(rel_path.parent, max_time=max_time)}/{path.name}'
|
|
return hash_file_path(rel_path, max_time=max_time)
|
|
|
|
|
|
def _get_name(path: Path) -> str:
|
|
return path.name.split('.', 1)[0]
|
|
|
|
|
|
def generate_resources(prefix: str, elements: Iterable[Element]) -> tuple[list[str],
|
|
list[str],
|
|
list[str],
|
|
dict[str, str],
|
|
list[str],
|
|
list[str]]:
|
|
"""Generate the resources required by the elements to be sent to the client."""
|
|
done_libraries: set[str] = set()
|
|
done_components: set[str] = set()
|
|
vue_scripts: list[str] = []
|
|
vue_html: list[str] = []
|
|
vue_styles: list[str] = []
|
|
imports: dict[str, str] = {
|
|
'vue': f'{prefix}/_nicegui/{__version__}/static/vue.esm-browser{".prod" if core.app.config.prod_js else ""}.js',
|
|
'sass': f'{prefix}/_nicegui/{__version__}/static/sass.default.js',
|
|
'immutable': f'{prefix}/_nicegui/{__version__}/static/immutable.es.js',
|
|
}
|
|
js_imports: list[str] = []
|
|
js_imports_urls: list[str] = [imports['vue']]
|
|
|
|
# build the importmap structure for libraries
|
|
for key, library in libraries.items():
|
|
if key not in done_libraries:
|
|
imports[library.name] = f'{prefix}/_nicegui/{__version__}/libraries/{key}'
|
|
done_libraries.add(key)
|
|
|
|
# build the importmap structure for ESM modules
|
|
for key, esm_module in esm_modules.items():
|
|
imports[f'{esm_module.name}'] = f'{prefix}/_nicegui/{__version__}/esm/{key}/index.js'
|
|
imports[f'{esm_module.name}/'] = f'{prefix}/_nicegui/{__version__}/esm/{key}/'
|
|
|
|
# build the none-optimized component (i.e. the Vue component)
|
|
for key, vue_component in vue_components.items():
|
|
if key not in done_components:
|
|
vue_html.append(vue_component.html)
|
|
url = f'{prefix}/_nicegui/{__version__}/components/{vue_component.key}'
|
|
js_imports.append(f'import {{ default as {vue_component.name} }} from "{url}";')
|
|
js_imports.append(f"{vue_component.name}.template = '#tpl-{vue_component.name}';")
|
|
js_imports.append(f'app.component("{vue_component.tag}", {vue_component.name});')
|
|
js_imports_urls.append(url)
|
|
vue_styles.append(vue_component.style)
|
|
done_components.add(key)
|
|
|
|
# build the resources associated with the elements
|
|
for element in elements:
|
|
if element.component:
|
|
js_component = element.component
|
|
if js_component.key not in done_components and js_component.path.suffix.lower() == '.js':
|
|
url = f'{prefix}/_nicegui/{__version__}/components/{js_component.key}'
|
|
js_imports.append(f'import {{ default as {js_component.name} }} from "{url}";')
|
|
js_imports.append(f'app.component("{js_component.tag}", {js_component.name});')
|
|
js_imports_urls.append(url)
|
|
done_components.add(js_component.key)
|
|
return vue_html, vue_styles, vue_scripts, imports, js_imports, js_imports_urls
|