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 )