<filename>pysvelte/javascript.py
import stat
import subprocess
import threading

from .vis_paths import PYSVELTE_ROOT, Path

NODE_MODULES = PYSVELTE_ROOT / "node_modules"
PACKAGE_JSON = PYSVELTE_ROOT / "package.json"
SRC = PYSVELTE_ROOT / "src"
DIST = PYSVELTE_ROOT / "dist"


def mtime(path: Path):
    try:
        st = path.stat()
    except FileNotFoundError:
        return 0
    if stat.S_ISDIR(st.st_mode):
        fs = list(path.rglob("*"))
        if fs:
            return max(mtime(f) for f in fs)
        else:
            return 0
    else:
        return st.st_mtime


def is_npm_install_necessary():
    """Check if npm dependencies are out of date or missing."""
    if not NODE_MODULES.exists():
        return True
    return mtime(NODE_MODULES) < mtime(PACKAGE_JSON)


def install_if_necessary():
    """Install npm modules if they're out of date or missing."""
    if is_npm_install_necessary():
        print("Running npm install...")
        subprocess.check_call(["npm", "--prefix", str(PYSVELTE_ROOT), "install"])


vis_build_lock = threading.Lock()


def webpack_if_necessary(paths=None):
    """Use webpack to rebuild visualization components if missing or out of date.

    Args:
      paths: Assets to build if necesary. If None (the default) we build all.
    """
    with vis_build_lock:
        dists = [DIST / p for p in paths] or [DIST]
        stale = any(
            mtime(dist) < mtime(SRC) or mtime(dist) < mtime(PACKAGE_JSON)
            for dist in dists
        )
        if stale:
            print("pysvelte components appear to be unbuilt or stale")
            install_if_necessary()
            print("Building pysvelte components with webpack...")
            if paths:
                entries = [p.split("/")[-1].replace(".js", "") for p in paths]
                entries = ",".join(entries)
                env_flag = [f"--env=entry={entries}"]
            else:
                env_flag = []
            subprocess.check_call(["npx", "webpack"] + env_flag, cwd=str(PYSVELTE_ROOT))


def get_src_path(name):
    f = SRC / f"{name}.svelte"
    if f.exists():
        return f
    f = SRC / f"{name}/main.svelte"
    if f.exists():
        return f


def get_dist_path(name):
    return DIST / f"{name}.js"


def load_dist_path(path):
    webpack_if_necessary([path])
    dev_path = DIST / path
    if not dev_path.exists():
        msg = f"Could not find the built file '{path}' "
        raise Exception(msg)
    with dev_path.open() as f:
        return f.read()


def dev_url(host, path):
    if host[-1] != "/":
        host = host + "/"
    return host + path


def get_script_tag(path, dev_host=None):
    if not dev_host:
        return f"<script>{load_dist_path(path)}</script>"
    else:
        return f"<script src='{dev_url(dev_host, path)}'></script>"


def get_script_tags(paths, dev_host=None) -> str:
    """Get html <script> tags to load the visualizations at paths.

    Args:
      paths: paths to assets to be built, assumed to be in DIST.
      dev_host: If this value is set (not None), we are assumed to be
        in "dev mode". Rather than inlining the javascript, we try to
        load it from a url based on dev_host.
    """
    if dev_host is None:
        # Although the get_script_tag() below would also trigger builds,
        # doing it like this builds all needed assets in one pass which is
        # significantly faster.
        webpack_if_necessary(paths)
    tags = [get_script_tag(path, dev_host=dev_host) for path in paths]
    return "\n".join(tags)

<filename>pysvelte/javascript.py
import stat
import subprocess
import threading

from .vis_paths import PYSVELTE_ROOT, Path

NODE_MODULES = PYSVELTE_ROOT / "node_modules"
PACKAGE_JSON = PYSVELTE_ROOT / "package.json"
SRC = PYSVELTE_ROOT / "src"
DIST = PYSVELTE_ROOT / "dist"


def mtime(path: Path):
    try:
        st = path.stat()
    except FileNotFoundError:
        return 0
    if stat.S_ISDIR(st.st_mode):
        fs = list(path.rglob("*"))
        if fs:
            return max(mtime(f) for f in fs)
        else:
            return 0
    else:
        return st.st_mtime


def is_npm_install_necessary():
    """Check if npm dependencies are out of date or missing."""
    if not NODE_MODULES.exists():
        return True
    return mtime(NODE_MODULES) < mtime(PACKAGE_JSON)


def install_if_necessary():
    """Install npm modules if they're out of date or missing."""
    if is_npm_install_necessary():
        print("Running npm install...")
        subprocess.check_call(["npm", "--prefix", str(PYSVELTE_ROOT), "install"])


vis_build_lock = threading.Lock()


def webpack_if_necessary(paths=None):
    """Use webpack to rebuild visualization components if missing or out of date.

    Args:
      paths: Assets to build if necesary. If None (the default) we build all.
    """
    with vis_build_lock:
        dists = [DIST / p for p in paths] or [DIST]
        stale = any(
            mtime(dist) < mtime(SRC) or mtime(dist) < mtime(PACKAGE_JSON)
            for dist in dists
        )
        if stale:
            print("pysvelte components appear to be unbuilt or stale")
            install_if_necessary()
            print("Building pysvelte components with webpack...")
            if paths:
                entries = [p.split("/")[-1].replace(".js", "") for p in paths]
                entries = ",".join(entries)
                env_flag = [f"--env=entry={entries}"]
            else:
                env_flag = []
            subprocess.check_call(["npx", "webpack"] + env_flag, cwd=str(PYSVELTE_ROOT))


def get_src_path(name):
    f = SRC / f"{name}.svelte"
    if f.exists():
        return f
    f = SRC / f"{name}/main.svelte"
    if f.exists():
        return f


def get_dist_path(name):
    return DIST / f"{name}.js"


def load_dist_path(path):
    webpack_if_necessary([path])
    dev_path = DIST / path
    if not dev_path.exists():
        msg = f"Could not find the built file '{path}' "
        raise Exception(msg)
    with dev_path.open() as f:
        return f.read()


def dev_url(host, path):
    if host[-1] != "/":
        host = host + "/"
    return host + path


def get_script_tag(path, dev_host=None):
    if not dev_host:
        return f"<script>{load_dist_path(path)}</script>"
    else:
        return f"<script src='{dev_url(dev_host, path)}'></script>"


def get_script_tags(paths, dev_host=None) -> str:
    """Get html <script> tags to load the visualizations at paths.

    Args:
      paths: paths to assets to be built, assumed to be in DIST.
      dev_host: If this value is set (not None), we are assumed to be
        in "dev mode". Rather than inlining the javascript, we try to
        load it from a url based on dev_host.
    """
    if dev_host is None:
        # Although the get_script_tag() below would also trigger builds,
        # doing it like this builds all needed assets in one pass which is
        # significantly faster.
        webpack_if_necessary(paths)
    tags = [get_script_tag(path, dev_host=dev_host) for path in paths]
    return "\n".join(tags)

Check if npm dependencies are out of date or missing. Install npm modules if they're out of date or missing. Use webpack to rebuild visualization components if missing or out of date.      Args:       paths: Assets to build if necesary. If None (the default) we build all. Get html <script> tags to load the visualizations at paths.      Args:       paths: paths to assets to be built, assumed to be in DIST.       dev_host: If this value is set (not None), we are assumed to be         in "dev mode". Rather than inlining the javascript, we try to         load it from a url based on dev_host. # Although the get_script_tag() below would also trigger builds, # doing it like this builds all needed assets in one pass which is # significantly faster.