v0.165.0
  1import shutil
  2import subprocess
  3import sys
  4import tomllib
  5from importlib.metadata import entry_points
  6from pathlib import Path
  7
  8import click
  9import plain.runtime
 10from plain.cli import register_cli
 11from plain.cli.print import print_event
 12
 13from .compile import compile_assets, get_compiled_path
 14
 15
 16@register_cli("assets")
 17@click.group()
 18def cli() -> None:
 19    """Asset management"""
 20
 21
 22@cli.command(name="compile")
 23@click.option(
 24    "--keep-original/--no-keep-original",
 25    "keep_original",
 26    is_flag=True,
 27    default=False,
 28    help="Keep the original assets",
 29)
 30@click.option(
 31    "--fingerprint/--no-fingerprint",
 32    "fingerprint",
 33    is_flag=True,
 34    default=True,
 35    help="Fingerprint the assets",
 36)
 37@click.option(
 38    "--compress/--no-compress",
 39    "compress",
 40    is_flag=True,
 41    default=True,
 42    help="Compress the assets",
 43)
 44def compile_cmd(keep_original: bool, fingerprint: bool, compress: bool) -> None:
 45    """Run pre-compile hooks, then fingerprint and compress assets."""
 46
 47    if not keep_original and not fingerprint:
 48        raise click.UsageError(
 49            "You must either keep the original assets or fingerprint them."
 50        )
 51
 52    # Run user-defined pre-compile shell commands first
 53    pyproject_path = plain.runtime.APP_PATH.parent / "pyproject.toml"
 54    if pyproject_path.exists():
 55        with pyproject_path.open("rb") as f:
 56            pyproject = tomllib.load(f)
 57
 58        for name, data in (
 59            pyproject.get("tool", {})
 60            .get("plain", {})
 61            .get("assets", {})
 62            .get("run", {})
 63            .items()
 64        ):
 65            print_event(f"{name}...")
 66            result = subprocess.run(data["cmd"], shell=True, check=False)
 67            if result.returncode:
 68                click.secho(f"Error in {name} (exit {result.returncode})", fg="red")
 69                sys.exit(result.returncode)
 70
 71    # Then run installed package pre-compile hooks (like tailwind)
 72    for entry_point in entry_points(group="plain.assets.compile"):
 73        print_event(f"{entry_point.name}...")
 74        entry_point.load()()
 75
 76    # Compile assets
 77    target_dir = get_compiled_path()
 78    click.secho(f"Compiling assets to {target_dir}", bold=True)
 79    if target_dir.exists():
 80        click.secho("(clearing previously compiled assets)")
 81        shutil.rmtree(target_dir)
 82    target_dir.mkdir(parents=True, exist_ok=True)
 83
 84    total_files = 0
 85    total_compiled = 0
 86
 87    for url_path, resolved_url_path, compiled_paths in compile_assets(
 88        target_dir=str(target_dir),
 89        keep_original=keep_original,
 90        fingerprint=fingerprint,
 91        compress=compress,
 92    ):
 93        if url_path == resolved_url_path:
 94            click.secho(url_path, bold=True)
 95        else:
 96            click.secho(url_path, bold=True, nl=False)
 97            click.secho(" → ", fg="yellow", nl=False)
 98            click.echo(resolved_url_path)
 99
100        print("\n".join(f"  {Path(p).relative_to(Path.cwd())}" for p in compiled_paths))
101
102        total_files += 1
103        total_compiled += len(compiled_paths)
104
105    click.secho(
106        f"\nCompiled {total_files} assets into {total_compiled} files", fg="green"
107    )