|
@@ -1,102 +1,135 @@
|
|
|
-import json
|
|
|
-import logging
|
|
|
+from dataclasses import dataclass
|
|
|
from pathlib import Path
|
|
|
-from typing import Any, Dict, List, Set, Tuple
|
|
|
+from typing import Dict, List, Set, Tuple
|
|
|
|
|
|
import vbuild
|
|
|
|
|
|
from . import __version__
|
|
|
from .element import Element
|
|
|
+from .helpers import KWONLY_SLOTS
|
|
|
|
|
|
-vue_components: Dict[str, Any] = {}
|
|
|
-js_components: Dict[str, Any] = {}
|
|
|
-libraries: Dict[str, Any] = {}
|
|
|
|
|
|
+@dataclass(**KWONLY_SLOTS)
|
|
|
+class Component:
|
|
|
+ key: str
|
|
|
+ name: str
|
|
|
|
|
|
-def register_vue_component(location: Path,
|
|
|
- base_path: Path = Path(__file__).parent / 'elements', *, expose: bool = False
|
|
|
- ) -> str:
|
|
|
+ @property
|
|
|
+ def tag(self) -> str:
|
|
|
+ return f'nicegui-{self.name}'
|
|
|
+
|
|
|
+
|
|
|
+@dataclass(**KWONLY_SLOTS)
|
|
|
+class VueComponent(Component):
|
|
|
+ html: str
|
|
|
+ script: str
|
|
|
+ style: str
|
|
|
+
|
|
|
+
|
|
|
+@dataclass(**KWONLY_SLOTS)
|
|
|
+class JsComponent(Component):
|
|
|
+ path: Path
|
|
|
+
|
|
|
+
|
|
|
+@dataclass(**KWONLY_SLOTS)
|
|
|
+class Library:
|
|
|
+ key: str
|
|
|
+ name: str
|
|
|
+ path: Path
|
|
|
+ expose: bool
|
|
|
+
|
|
|
+
|
|
|
+vue_components: Dict[str, VueComponent] = {}
|
|
|
+js_components: Dict[str, JsComponent] = {}
|
|
|
+libraries: Dict[str, Library] = {}
|
|
|
+
|
|
|
+
|
|
|
+def register_vue_component(location: Path, base_path: Path = Path(__file__).parent / 'elements') -> Component:
|
|
|
"""Register a .vue or .js Vue component.
|
|
|
|
|
|
- :param location: the location to the library you want to register relative to the base_path. This is also used as the resource identifier and must therefore be url-safe.
|
|
|
- :param base_path: the base path where your libraries are located
|
|
|
- :return: the resource identifier library name to be used in element's `use_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.
|
|
|
+
|
|
|
+ :param location: location to the library relative to the base_path (used as the resource identifier, must be URL-safe)
|
|
|
+ :param base_path: base path where your libraries are located
|
|
|
+ :return: resource identifier to be used in element's `use_component`
|
|
|
"""
|
|
|
- if isinstance(location, str):
|
|
|
- logging.warning('register_vue_component: location is a string, did you mean to use register_library?')
|
|
|
- return
|
|
|
- suffix = location.suffix.lower()
|
|
|
- assert suffix in {'.vue', '.js', '.mjs'}, 'Only VUE and JS components are supported.'
|
|
|
- name = location.stem
|
|
|
- path = base_path / location
|
|
|
+ path, key, name, suffix = deconstruct_location(location, base_path)
|
|
|
if suffix == '.vue':
|
|
|
- assert name not in vue_components, f'Duplicate VUE component name {name}'
|
|
|
- # The component (in case of .vue) is built right away to:
|
|
|
- # 1. delegate this "long" process to the bootstrap phase
|
|
|
- # 2. avoid building the component on every single request
|
|
|
- vue_components[name] = vbuild.VBuild(name, path.read_text())
|
|
|
- elif suffix == '.js':
|
|
|
- assert name not in js_components, f'Duplicate JS component name {name}'
|
|
|
- js_components[str(location)] = {'name': name, 'path': path}
|
|
|
- return str(location)
|
|
|
-
|
|
|
-
|
|
|
-def register_library(location: Path,
|
|
|
- base_path: Path = Path(__file__).parent / 'elements' / 'lib', *, expose: bool = False
|
|
|
- ) -> str:
|
|
|
- """Register a new external library.
|
|
|
-
|
|
|
- :param location: the location to the library you want to register relative to the base_path. This is also used as the resource identifier and must therefore be url-safe.
|
|
|
- :param base_path: the base path where your libraries are located
|
|
|
- :param expose: if True, this will be exposed as an ESM module but NOT imported
|
|
|
- :return: the resource identifier library name to be used in element's `use_library`
|
|
|
+ assert key not in vue_components, f'Duplicate VUE component {key}'
|
|
|
+ build = vbuild.VBuild(name, path.read_text())
|
|
|
+ vue_components[key] = VueComponent(key=key, name=name, html=build.html, script=build.script, style=build.style)
|
|
|
+ return vue_components[key]
|
|
|
+ if suffix == '.js':
|
|
|
+ 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 "{suffix}"')
|
|
|
+
|
|
|
+
|
|
|
+def register_library(location: Path, base_path: Path = Path(__file__).parent / 'elements' / 'lib', *,
|
|
|
+ expose: bool = False) -> Library:
|
|
|
+ """Register a *.js library.
|
|
|
+
|
|
|
+ :param location: location to the library relative to the base_path (used as the resource identifier, must be URL-safe)
|
|
|
+ :param base_path: base path where your libraries are located
|
|
|
+ :param expose: whether to expose library as an ESM module (exposed modules will NOT be imported)
|
|
|
+ :return: resource identifier to be used in element's `use_library`
|
|
|
"""
|
|
|
- if isinstance(location, str):
|
|
|
- return
|
|
|
- assert location.suffix == '.js' or location.suffix == '.mjs', 'Only JS dependencies are supported.'
|
|
|
- name = str(location)
|
|
|
- assert name not in libraries, f'Duplicate js library name {name}'
|
|
|
- libraries[name] = {'name': name, 'path': base_path / location, 'expose': expose}
|
|
|
- return name
|
|
|
+ path, key, name, suffix = deconstruct_location(location, base_path)
|
|
|
+ if suffix in {'.js', '.mjs'}:
|
|
|
+ assert key not in libraries, f'Duplicate js library {key}'
|
|
|
+ libraries[key] = Library(key=key, name=name, path=path, expose=expose)
|
|
|
+ return libraries[key]
|
|
|
+ raise ValueError(f'Unsupported library type "{suffix}"')
|
|
|
+
|
|
|
|
|
|
+def deconstruct_location(location: Path, base_path: Path) -> Tuple[Path, str, str, str]:
|
|
|
+ """Deconstruct a location into its parts: full path, relative path, name, suffix."""
|
|
|
+ return base_path / location, str(location), location.name.split('.', 1)[0], location.suffix.lower()
|
|
|
|
|
|
-def generate_resources(prefix: str, elements: List[Element]) -> Tuple[str, str, str, str, str]:
|
|
|
+
|
|
|
+def generate_resources(prefix: str, elements: List[Element]) -> Tuple[List[str],
|
|
|
+ List[str],
|
|
|
+ List[str],
|
|
|
+ Dict[str, str],
|
|
|
+ List[str]]:
|
|
|
done_libraries: Set[str] = set()
|
|
|
done_components: Set[str] = set()
|
|
|
- vue_scripts = ''
|
|
|
- vue_html = ''
|
|
|
- vue_styles = ''
|
|
|
- js_imports = ''
|
|
|
- import_maps = {'imports': {}}
|
|
|
-
|
|
|
- # Build the importmap structure for exposed libraries.
|
|
|
- for resource in libraries:
|
|
|
- if resource not in done_libraries and libraries[resource]['expose']:
|
|
|
- name = libraries[resource]['name']
|
|
|
- import_maps['imports'][name] = f'{prefix}/_nicegui/{__version__}/library/{resource}'
|
|
|
- done_libraries.add(resource)
|
|
|
- # Build the none optimized component (ie, the vue component).
|
|
|
- for resource in vue_components:
|
|
|
- if resource not in done_components:
|
|
|
- vue_html += f'{vue_components[resource].html}\n'
|
|
|
- vue_scripts += f'{vue_components[resource].script.replace("Vue.component", "app.component", 1)}\n'
|
|
|
- vue_styles += f'{vue_components[resource].style}\n'
|
|
|
- done_components.add(resource)
|
|
|
-
|
|
|
- # Build the resources associated with the elements.
|
|
|
+ vue_scripts: List[str] = []
|
|
|
+ vue_html: List[str] = []
|
|
|
+ vue_styles: List[str] = []
|
|
|
+ js_imports: List[str] = []
|
|
|
+ imports: Dict[str, str] = {}
|
|
|
+
|
|
|
+ # build the importmap structure for exposed libraries
|
|
|
+ for key, library in libraries.items():
|
|
|
+ if key not in done_libraries and library.expose:
|
|
|
+ imports[library.name] = f'{prefix}/_nicegui/{__version__}/libraries/{key}'
|
|
|
+ done_libraries.add(key)
|
|
|
+
|
|
|
+ # build the none-optimized component (i.e. the Vue component)
|
|
|
+ for key, component in vue_components.items():
|
|
|
+ if key not in done_components:
|
|
|
+ vue_html.append(component.html)
|
|
|
+ vue_scripts.append(component.script.replace(f"Vue.component('{component.name}',",
|
|
|
+ f"app.component('{component.tag}',", 1))
|
|
|
+ vue_styles.append(component.style)
|
|
|
+ done_components.add(key)
|
|
|
+
|
|
|
+ # build the resources associated with the elements
|
|
|
for element in elements:
|
|
|
- for resource in element.libraries:
|
|
|
- if resource in libraries and resource not in done_libraries:
|
|
|
- if not libraries[resource]['expose']:
|
|
|
- js_imports += f'import "{prefix}/_nicegui/{__version__}/library/{resource}";\n'
|
|
|
- done_libraries.add(resource)
|
|
|
- for resource in element.components:
|
|
|
- if resource in js_components and resource not in done_components:
|
|
|
- name = js_components[resource]['name']
|
|
|
- var = name.replace('-', '_')
|
|
|
- js_imports += f'import {{ default as {var} }} from "{prefix}/_nicegui/{__version__}/components/{resource}";\n'
|
|
|
- js_imports += f'app.component("{name}", {var});\n'
|
|
|
- done_components.add(resource)
|
|
|
- vue_styles = f'<style>{vue_styles}</style>'
|
|
|
- import_maps = f'<script type="importmap">{json.dumps(import_maps)}</script>'
|
|
|
- return vue_html, vue_styles, vue_scripts, import_maps, js_imports
|
|
|
+ for library in element.libraries:
|
|
|
+ if library.key not in done_libraries:
|
|
|
+ if not library.expose:
|
|
|
+ js_imports.append(f'import "{prefix}/_nicegui/{__version__}/libraries/{library.key}";')
|
|
|
+ done_libraries.add(library.key)
|
|
|
+ for component in element.components:
|
|
|
+ if component.key not in done_components:
|
|
|
+ js_imports.extend([
|
|
|
+ f'import {{ default as {component.name} }} from "{prefix}/_nicegui/{__version__}/components/{component.key}";',
|
|
|
+ f'app.component("{component.tag}", {component.name});',
|
|
|
+ ])
|
|
|
+ done_components.add(component.key)
|
|
|
+ return vue_html, vue_styles, vue_scripts, imports, js_imports
|