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