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