Plain is headed towards 1.0! Subscribe for development updates →

  1from __future__ import annotations
  2
  3from pathlib import Path
  4
  5import click
  6
  7from plain.assets.finders import APP_ASSETS_DIR
  8from plain.cli import register_cli
  9
 10from .deps import Dependency, get_deps
 11from .exceptions import DependencyError
 12
 13VENDOR_DIR = APP_ASSETS_DIR / "vendor"
 14
 15
 16@register_cli("vendor")
 17@click.group()
 18def cli() -> None:
 19    """Vendor CSS/JS from a CDN"""
 20    pass
 21
 22
 23@cli.command()
 24def sync() -> None:
 25    """Clear vendored assets and re-download"""
 26    click.secho("Clearing existing vendored dependencies...", bold=True)
 27    if VENDOR_DIR.exists():
 28        for path in VENDOR_DIR.iterdir():
 29            path.unlink()
 30
 31    deps = get_deps()
 32    if not deps:
 33        click.echo(
 34            "No vendored dependencies found in pyproject.toml. Use [tool.plain.vendor.dependencies]"
 35        )
 36        return
 37
 38    errors = []
 39
 40    for dep in deps:
 41        click.secho(f"Installing {dep.name}...", bold=True, nl=False)
 42        try:
 43            vendored_path = dep.install()
 44        except DependencyError as e:
 45            click.secho(f"  {e}", fg="red")
 46            errors.append(e)
 47
 48        vendored_path = vendored_path.relative_to(Path.cwd())
 49
 50        click.secho(f" {dep.installed}", fg="green", nl=False)
 51        click.secho(f" -> {vendored_path}")
 52
 53    if errors:
 54        click.secho("Failed to install some dependencies.", fg="red")
 55        exit(1)
 56
 57
 58@cli.command()
 59@click.argument("name", nargs=-1, default=None)
 60def update(name: tuple[str, ...]) -> None:
 61    """Update vendored dependencies in pyproject.toml"""
 62    deps = get_deps()
 63    if not deps:
 64        click.echo(
 65            "No vendored dependencies found in pyproject.toml. Use [tool.plain.vendor.dependencies]"
 66        )
 67        return
 68
 69    errors = []
 70
 71    if name:
 72        deps = [dep for dep in deps if dep.name in name]
 73        if len(deps) != len(name):
 74            not_found = set(name) - {dep.name for dep in deps}
 75            raise click.UsageError(
 76                f"Some dependencies not found: {', '.join(not_found)}"
 77            )
 78
 79    for dep in deps:
 80        click.secho(f"Updating {dep.name} {dep.installed}...", bold=True, nl=False)
 81        try:
 82            vendored_path = dep.update()
 83            vendored_path = vendored_path.relative_to(Path.cwd())
 84
 85            click.secho(f" {dep.installed}", fg="green", nl=False)
 86            click.secho(f" -> {vendored_path}")
 87        except DependencyError as e:
 88            click.secho(f"  {e}", fg="red")
 89            errors.append(e)
 90
 91    if errors:
 92        click.secho("Failed to install some dependencies.", fg="red")
 93        exit(1)
 94
 95
 96@cli.command()
 97@click.argument("url")
 98@click.option("--name", help="Name of the dependency")
 99@click.option("--sourcemap/--no-sourcemap", default=True, help="Download sourcemap")
100def add(url: str, name: str | None, sourcemap: bool) -> None:
101    """Add a new vendored dependency to pyproject.toml"""
102    if not name:
103        name = url.split("/")[-1].split("?")[0].split("#")[0]
104
105    dep = Dependency(name, url=url, sourcemap=sourcemap)
106
107    click.secho(f"Installing {dep.name}", bold=True, nl=False)
108
109    try:
110        vendored_path = dep.update()
111    except DependencyError as e:
112        click.secho(f"  {e}", fg="red")
113        exit(1)
114
115    vendored_path = vendored_path.relative_to(Path.cwd())
116
117    click.secho(f" {dep.installed}", fg="green", nl=False)
118    click.secho(f" -> {vendored_path}")
119
120    if not dep.installed:
121        click.secho(
122            "No version was parsed from the url. You can configure it manually if you need to.",
123            fg="yellow",
124        )