Manifest Compiler
=================

:meth:`~xclif.Cli.from_routes` discovers commands by walking your routes package with
``pkgutil.walk_packages`` on every invocation. For most CLIs this is imperceptible, but it
adds ~13–15 ms of cold-start overhead on Apple Silicon — enough to matter if your tool is
invoked hundreds of times per second (e.g. shell completion hooks or CI scripts).

The **manifest compiler** eliminates this overhead. It walks the package *once* at build time
and writes a static ``_xclif_manifest.py`` file. At runtime, loading the manifest is a plain
Python import — no filesystem walk, no ``inspect.getmembers``.

When to use it
--------------

Consider compiling a manifest when:

* Shell completion feels sluggish, because the completion hook spawns your CLI on every keypress.
* Your tool is called in tight loops (CI scripts, ``git`` hooks, makefiles).
* You ship a compiled package (wheel) and want the fastest possible startup.

For interactive development, ``from_routes`` is easier — no extra build step, and route changes
are picked up automatically.

Compiling a manifest
--------------------

**From the command line** (recommended):

.. code-block:: bash

   python -m xclif compile myapp.routes

This imports ``myapp.routes`` and writes ``_xclif_manifest.py`` next to the routes package
(i.e. inside the ``myapp/`` directory).  Pass ``--output <dir>`` to write elsewhere:

.. code-block:: bash

   python -m xclif compile myapp.routes --output src/myapp

**Programmatically** (e.g. in a build script):

.. code-block:: python

   from xclif.compiler import compile_routes
   import myapp.routes as routes

   path = compile_routes(routes)
   print(f"Manifest written to {path}")

Either way, commit the generated file to version control so it is available in installed
distributions without a separate compilation step.

Loading the manifest at runtime
--------------------------------

Replace :meth:`~xclif.Cli.from_routes` with :meth:`~xclif.Cli.from_manifest` in your entry
point:

.. code-block:: python

   # myapp/__main__.py
   from xclif import Cli
   from myapp import _xclif_manifest

   cli = Cli.from_manifest(_xclif_manifest)
   if __name__ == "__main__":
       cli()

:meth:`~xclif.Cli.from_manifest` calls the ``_build_cli()`` function inside the manifest
module, which lazily imports each route and assembles the command tree — exactly the same tree
that ``from_routes`` would produce.

What the manifest looks like
-----------------------------

The generated ``_xclif_manifest.py`` is readable Python. Given a routes package with this
layout:

.. code-block:: text

   myapp/routes/
   ├── __init__.py        →  myapp          (root)
   ├── greet.py           →  myapp greet
   └── config/
       ├── __init__.py    →  myapp config   (group)
       ├── get.py         →  myapp config get
       └── set.py         →  myapp config set

The compiler writes:

.. code-block:: python

   # Generated by `xclif compile` — do not edit by hand.
   # Re-run `python -m xclif compile <routes_module>` after adding/removing routes.
   from __future__ import annotations

   from xclif import Cli


   def _build_cli(version: str | None = None) -> Cli:
       from myapp.routes import _ as _root
       from myapp.routes.greet import _ as _myapp_routes_greet
       from myapp.routes.config import _ as _myapp_routes_config
       from myapp.routes.config.get import _ as _myapp_routes_config_get
       from myapp.routes.config.set import _ as _myapp_routes_config_set

       root = _root

       cli = Cli(root_command=root, version=version)
       cli.add_command(['config'], _myapp_routes_config)
       cli.add_command(['greet'], _myapp_routes_greet)
       cli.add_command(['config', 'get'], _myapp_routes_config_get)
       cli.add_command(['config', 'set'], _myapp_routes_config_set)
       return cli

The imports are **inside** ``_build_cli()`` so loading the manifest module itself is free —
route modules are only imported when you call ``_build_cli()``, matching the laziness of
``from_routes``.

Keeping the manifest up to date
--------------------------------

Re-run ``xclif compile`` after **adding, removing, or renaming** any route file. The manifest
does not self-update.

A convenient place to hook this in is your build system. For example, with ``pyproject.toml``
and `hatch <https://hatch.pypa.io>`_:

.. code-block:: toml

   [tool.hatch.build.hooks.custom]
   # runs `python -m xclif compile myapp.routes` before building the wheel

Or add a ``Makefile`` target:

.. code-block:: makefile

   manifest:
       python -m xclif compile myapp.routes

If you use a CI pipeline, run the compile step before running tests so the manifest under test
matches the current routes.

.. tip::

   Run ``python -m xclif compile --help`` to see all available options.

API reference
-------------

.. automodule:: xclif.compiler
   :members:
   :undoc-members:
   :show-inheritance:
