view libervia/web/server/tasks/implicit/task_js_modules.py @ 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 eb00d593801d
children d07838fc9d99
line wrap: on
line source

#!/ur/bin/env python3

import json
from pathlib import Path
from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger
from libervia.backend.core import exceptions
from libervia.web.server.constants import Const as C
from libervia.web.server.tasks import task


log = getLogger(__name__)


class Task(task.Task):

    async def prepare(self):
        if "js" not in self.resource.browser_modules:
            raise exceptions.CancelError("No JS module needed")

    async def start(self):
        js_data = self.resource.browser_modules['js']
        package = js_data.get('package', {})
        package_path = self.build_path / 'package.json'
        with package_path.open('w') as f:
            json.dump(package, f)

        cmd = self.find_command('yarnpkg', 'yarn')
        await self.runCommand(cmd, 'install', path=str(self.build_path))

        try:
            brython_map = js_data['brython_map']
        except KeyError:
            pass
        else:
            log.info(_("creating JS modules mapping for Brython"))
            js_modules_path = self.build_path / 'js_modules'
            js_modules_path.mkdir(exist_ok=True)
            init_path = js_modules_path / '__init__.py'
            init_path.touch()

            for module_name, module_data in brython_map.items():
                log.debug(f"generating mapping for {module_name}")
                if ' ' in module_name:
                    raise ValueError(
                        f"module {module_name!r} has space(s), it must not!")
                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]}
                try:
                    js_paths = module_data.pop('path')
                except KeyError:
                    raise ValueError(
                        f'module data for {module_name} must have a "path" key')
                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:
                    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}")

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