changeset 1570:038d4bfdd967

server (tasks/JS modules): add new way to generate modules + support of CSS files
author Goffi <goffi@goffi.org>
date Wed, 22 Nov 2023 16:31:32 +0100
parents 54ba0f74a488
children ff633e19a069
files libervia/web/pages/_browser/proxy.py libervia/web/server/tasks/implicit/task_js_modules.py
diffstat 2 files changed, 146 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/web/pages/_browser/proxy.py	Wed Nov 22 16:31:32 2023 +0100
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+
+# Libervia XMPP
+# Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+class JSProxy:
+    def __init__(self):
+        self._module = None
+
+    @property
+    def js_module(self):
+        return self._module
+
+    @js_module.setter
+    def js_module(self, module):
+        if self._module is not None:
+            raise Exception("Module is already set!")
+        self._module = module
+
+    def __getattr__(self, name):
+        if self._module is None:
+            raise RuntimeError("The module has not been loaded yet")
+        return getattr(self._module, name)
+
+    def __call__(self, *args):
+        if self._module is None:
+            raise RuntimeError("The module has not been loaded yet")
+        return self._module(*args)
--- a/libervia/web/server/tasks/implicit/task_js_modules.py	Wed Nov 22 15:25:52 2023 +0100
+++ b/libervia/web/server/tasks/implicit/task_js_modules.py	Wed Nov 22 16:31:32 2023 +0100
@@ -44,26 +44,116 @@
                 if ' ' in module_name:
                     raise ValueError(
                         f"module {module_name!r} has space(s), it must not!")
-                module_path = js_modules_path / f"{module_name}.py"
+                module_python_name = module_name.replace(".", "_").replace("-", "_")
+                if module_python_name != module_name:
+                    log.info("{module_python_name!r} will be used as python module name")
+                module_path = js_modules_path / f"{module_python_name}.py"
                 if isinstance(module_data, str):
-                    module_data = {'path': module_data}
+                    module_data = {'path': [module_data]}
                 try:
-                    js_path = module_data.pop('path')
+                    js_paths = module_data.pop('path')
                 except KeyError:
                     raise ValueError(
                         f'module data for {module_name} must have a "path" key')
-                module_data['path'] = Path('node_modules') / js_path.strip(' /')
-                export = module_data.get('export') or [module_name]
-                export_objects = '\n'.join(f'{e} = window.{e}' for e in export)
-                extra_kwargs = {"build_dir": C.BUILD_DIR}
+                if isinstance(js_paths, str):
+                    js_paths = [js_paths]
+                module_data['path'] = [
+                    Path('node_modules') / js_path.strip(' /') for js_path in js_paths
+                ]
+                to_export = module_data.get("export", [module_name])
+                if isinstance(to_export, str):
+                    to_export = to_export.split(",")
+                to_export = [e.strip() for e in to_export]
+
+                init_code = []
+
+                # CSS if any
+                css_to_load = module_data.get("css")
+                if css_to_load:
+                    if isinstance(css_to_load, str):
+                        css_to_load = [css_to_load]
+
+                    add_styles_lines = []
+                    for css_path in css_to_load:
+                        normalized_css_path = Path("node_modules") / css_path.strip(" /")
+                        css_href = Path("/").joinpath(C.BUILD_DIR, normalized_css_path)
+                        style_tag = (
+                            f'<link rel="stylesheet" type="text/css" href="{css_href}">'
+                        )
+                        add_style_code = (
+                            'document.head.insertAdjacentHTML('
+                            '"beforeend", '
+                            f"'{style_tag}')"
+                        )
+                        add_styles_lines.append(add_style_code)
+
+                    add_styles = "\n".join(add_styles_lines)
+                else:
+                    add_styles = ""
 
                 with module_path.open('w') as f:
-                    f.write(f"""\
-#!/usr/bin/env python3
-from browser import window, load
-{module_data.get('extra_import', '')}
+                    browser_imports = ["aio", "document"]
+                    modules_import = []
+                    script_paths = [
+                        str(Path('/').joinpath(C.BUILD_DIR, path))
+                        for path in module_data["path"]
+                    ]
+                    import_type = module_data.get('import_type', "load")
+                    if import_type == 'module':
+                        modules_import.append('javascript')
+                        modules_import.append('proxy')
+                        callback_function_name = f"on_{module_python_name}_loaded"
+                        declare_obj = "\n".join(f"{e} = proxy.JSProxy()" for e in to_export)
+                        export_str = "\n    ".join(f"{e}.js_module=module.{e}" for e in to_export)
+                        load_js_libraries = (
+                            f"\n{declare_obj}\n\n"
+                            f"def {callback_function_name}(module):\n"
+                            f"    {export_str}\n"
+                            f"    loaded.set_result(True)\n\n"
+                            f"javascript.import_modules({script_paths}, {callback_function_name})\n"
+                        )
+                    elif import_type == 'script':
+                        browser_imports.append("window")
+                        script_tags = [
+                            f"<script src='{src}'></script>" for src in script_paths
+                        ]
+                        load_js_libraries = "\n".join(
+                            f'document.head.insertAdjacentHTML("beforeend", "{tag}")'
+                            for tag in script_tags
+                        )
+                        init_code.append('\n'.join(f'{e} = window.{e}' for e in to_export))
+                        init_code.append("loaded.set_result(True)")
+                    elif import_type == "load":
+                        browser_imports.append("load")
+                        browser_imports.append("window")
+                        load_calls = [f'load("{src}")' for src in script_paths]
+                        load_js_libraries = "\n".join(load_calls)
+                        init_code.append('\n'.join(f'{e} = window.{e}' for e in to_export))
+                        init_code.append("loaded.set_result(True)")
+                    else:
+                        raise ValueError("Invalid import type: {import_type!r}")
 
-load("{Path('/').joinpath(C.BUILD_DIR, module_data['path'])}")
-{export_objects}
-{module_data.get('extra_init', '').format(**extra_kwargs)}
-""")
+                    extra_init = module_data.get("extra_init")
+                    if extra_init is not None:
+                        if not isinstance(extra_init, str):
+                            raise ValueError(f"Invalid extra_init: {extra_init!r}")
+                        init_code.append(
+                            extra_init.format(
+                                build_dir = C.BUILD_DIR
+                            )
+                        )
+
+                    init_code_str = "\n".join(init_code)
+                    imports = [f"from browser import {', '.join(browser_imports)}"]
+                    for module in modules_import:
+                        imports.append(f"import {module}")
+                    imports_str = "\n".join(imports)
+
+                    f.write(
+                        "#!/usr/bin/env python3\n"
+                        f"{imports_str}\n\n"
+                        f"loaded = aio.Future()\n"
+                        f"{add_styles}\n"
+                        f"{load_js_libraries}\n"
+                        f"{init_code_str}"
+                    )