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 )