Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .agents/skills/docs-conventions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ Calls :meth:`flet.Page.update` after modifying controls.

**Rules:**

- **Qualified reference:** `:class:`flet.Page`` — links to Page, displays `flet.Page`
- **Short display with `~`:** `:class:`~flet.Page`` — links to Page, displays just `Page`
- **Qualified reference:** `:attr:`flet.Page.route`` — links to the member, displays inline code `Page.route`
- **Short display with `~`:** `:attr:`~flet.Page.route`` — links to the member, displays inline code `route`
- **Local member (same class):** `:attr:`value`` — no qualifier needed
- **Method with parens:** `:meth:`update`` — do NOT include `()` in the target

For plain class references like `:class:`flet.Page``, the website strips the leading `flet.`
automatically, so both `:class:`flet.Page`` and `:class:`~flet.Page`` display as inline code `Page`.

**Not supported:**

- Custom labels like `:class:`my label <flet.Page>`` — the label is always auto-derived from the target
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

### Other changes

* Add `crocodocs watch` command for hot-reload docs development with file-watching, debounced regeneration, and optional child process management ([#6402](https://github.com/flet-dev/flet/pull/6402)) by @ndonkoHenri.
* Add a declarative `ReorderableListView` app example showing add, remove, and reorder flows with stable item identity.

## 0.84.0
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ tasks:
- uv run --group test pytest -s -o log_cli=true -o log_cli_level=DEBUG packages/flet/integration_tests

serve-docs:
desc: "Start Docusaurus dev server for the documentation website."
desc: "Start Docusaurus dev server for the documentation website with hot reload."
aliases:
- docs
cmds:
Expand Down
85 changes: 58 additions & 27 deletions tools/crocodocs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ nvm use 20
cd website
yarn install
yarn build # runs crocodocs generate + docusaurus build
yarn start # runs crocodocs generate + docusaurus dev server
yarn start # runs crocodocs watch + docusaurus dev server
```

### Running CrocoDocs directly
Expand All @@ -55,6 +55,32 @@ yarn start # runs crocodocs generate + docusaurus dev server
uv --directory ./tools/crocodocs run crocodocs generate
```

`yarn start` runs the Docusaurus dev server through `crocodocs watch`, which:

- performs an initial `generate`
- watches CrocoDocs inputs for changes
- regenerates API data, sidebars, partials, manifests, and synced assets after saves

Watched inputs include:

- `website/docs`
- `website/sidebars.yml`
- `sdk/python/packages/*/src`
- `sdk/python/examples`
- configured CrocoDocs asset source directories

Watch and regenerate without starting Docusaurus:

```bash
uv --directory ./tools/crocodocs run crocodocs watch
```

Watch and run a child process, using `--` to separate the child command:

```bash
uv --directory ./tools/crocodocs run crocodocs watch --child-cwd ../../website -- yarn exec docusaurus start
```

## Configuration

CrocoDocs is configured in [tools/crocodocs/pyproject.toml](./pyproject.toml).
Expand All @@ -63,17 +89,17 @@ CrocoDocs is configured in [tools/crocodocs/pyproject.toml](./pyproject.toml).

Core paths and settings:

| Key | Purpose |
|-----|---------|
| `docs_path` | Path to `website/docs` |
| `api_output` | Where to write `api-data.json` |
| `manifest_output` | Where to write `docs-manifest.json` |
| Key | Purpose |
|-----------------------|------------------------------------------|
| `docs_path` | Path to `website/docs` |
| `api_output` | Where to write `api-data.json` |
| `manifest_output` | Where to write `docs-manifest.json` |
| `partials_output_dir` | Where to write generated `.mdx` partials |
| `sidebars_source` | Path to `sidebars.yml` |
| `sidebars_output` | Path to generated `sidebars.js` |
| `base_url` | Base URL for docs routes (e.g. `/docs`) |
| `examples_root` | Path to code examples directory |
| `extensions` | Griffe extensions to load |
| `sidebars_source` | Path to `sidebars.yml` |
| `sidebars_output` | Path to generated `sidebars.js` |
| `base_url` | Base URL for docs routes (e.g. `/docs`) |
| `examples_root` | Path to code examples directory |
| `extensions` | Griffe extensions to load |

### `[tool.crocodocs.packages]`

Expand All @@ -83,11 +109,11 @@ Maps Python import names to source roots. These packages are scanned during API

Defines directories to bulk-copy into `website/static/docs/` during generate. Each mapping has:

| Key | Purpose |
|-----|---------|
| `source_path` | Source directory to copy from |
| `static_subpath` | Destination under `website/static/` |
| `include_exts` | File extensions to copy (e.g. `[".png", ".gif", ".svg"]`) |
| Key | Purpose |
|------------------|-----------------------------------------------------------|
| `source_path` | Source directory to copy from |
| `static_subpath` | Destination under `website/static/` |
| `include_exts` | File extensions to copy (e.g. `[".png", ".gif", ".svg"]`) |

Current mappings:

Expand All @@ -101,7 +127,8 @@ Controls which class members are hidden from API output (e.g. `init`, `before_up

## `sidebars.yml` Format

[website/sidebars.yml](../../website/sidebars.yml) is the hand-authored sidebar source. CrocoDocs generates [website/sidebars.js](../../website/sidebars.js) from it during `crocodocs generate`.
[website/sidebars.yml](../../website/sidebars.yml) is the hand-authored sidebar source.
CrocoDocs generates [website/sidebars.js](../../website/sidebars.js) from it during `crocodocs generate`.

### Rules

Expand Down Expand Up @@ -168,7 +195,11 @@ Python docstrings use reST-style cross-references:

Supported roles: `:class:`, `:attr:`, `:meth:`, `:func:`, `:data:`, `:mod:`, `:obj:`

The `~` prefix shortens the display to the last component (e.g. `Page` instead of `flet.Page`).
The website strips a leading `flet.` or `ft.` from rendered labels automatically
and renders reST API cross-references as inline code links
(for example, `:attr:`flet.Control.visible`` displays as ``Control.visible``).
The `~` prefix still shortens the display to the last component
(for example, `:attr:`~flet.Control.visible`` displays as ``visible``).

### Cross-references in Markdown

Expand Down Expand Up @@ -202,15 +233,15 @@ CrocoDocs generates data; the Docusaurus website renders it.

Website components in [website/src/components/crocodocs/](../../website/src/components/crocodocs/):

| Component | Purpose |
|-----------|---------|
| `ClassAll.js` | Full class page (summary + members) |
| `ClassSummary.js` | Class header with signature, image, inherits |
| `ClassMembers.js` | Properties, events, and methods listing |
| `ClassBlock.js` | Individual member rendering |
| `CodeExample.js` | Inline code example with syntax highlighting |
| `Image.js` | Doc image with `/docs/` prefix for root-relative paths |
| `utils.js` | Markdown rendering, xref resolution, admonition support |
| Component | Purpose |
|-------------------|---------------------------------------------------------|
| `ClassAll.js` | Full class page (summary + members) |
| `ClassSummary.js` | Class header with signature, image, inherits |
| `ClassMembers.js` | Properties, events, and methods listing |
| `ClassBlock.js` | Individual member rendering |
| `CodeExample.js` | Inline code example with syntax highlighting |
| `Image.js` | Doc image with `/docs/` prefix for root-relative paths |
| `utils.js` | Markdown rendering, xref resolution, admonition support |

### Key rendering features

Expand Down
1 change: 1 addition & 0 deletions tools/crocodocs/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies = [
"packaging>=23.0",
"PyYAML>=6.0",
"tomli >=2.0.1; python_version < '3.11'",
"watchdog >=4.0.0",
]

[project.scripts]
Expand Down
160 changes: 141 additions & 19 deletions tools/crocodocs/src/crocodocs/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,136 @@
load_config,
)
from .generate import run_generate
from .watch import run_watch


def _add_shared_generate_arguments(parser: argparse.ArgumentParser) -> None:
"""Register CLI options that both `generate` and `watch` understand."""
parser.add_argument(
"--docs-path",
metavar="PATH",
help="Path to the docs directory containing .md/.mdx files. "
"Overrides 'docs_path' from pyproject.toml.",
)
parser.add_argument(
"--manifest-output",
metavar="PATH",
help="Where to write the docs-manifest JSON file. "
"Overrides 'manifest_output' from pyproject.toml.",
)
parser.add_argument(
"--output",
metavar="PATH",
help="Where to write the api-data JSON file. "
"Overrides 'api_output' from pyproject.toml.",
)
parser.add_argument(
"--sidebars-source",
metavar="PATH",
help="Path to the sidebars YAML source file. "
"Overrides 'sidebars_source' from pyproject.toml.",
)
parser.add_argument(
"--sidebars-output",
metavar="PATH",
help="Where to write the generated sidebars.js file. "
"Overrides 'sidebars_output' from pyproject.toml.",
)
parser.add_argument(
"--base-url",
metavar="URL",
help="Base URL prefix for generated doc routes (e.g. '/docs'). "
"Overrides 'base_url' from pyproject.toml.",
)
parser.add_argument(
"--package",
action="append",
metavar="NAME:PATH",
help="Add or override a Python package source root for API extraction. "
"Can be specified multiple times. Example: --package flet:../../sdk/python/packages/flet/src",
)
parser.add_argument(
"--extensions",
action="append",
metavar="MODULE",
help="Griffe extension module to load during API extraction. "
"Can be specified multiple times.",
)


def _apply_shared_generate_overrides(config, args: argparse.Namespace) -> None:
"""Apply path/value/package overrides shared by generation-oriented commands."""
apply_path_override(config, "docs_path", args.docs_path)
apply_path_override(config, "manifest_output", args.manifest_output)
apply_path_override(config, "api_output", args.output)
apply_path_override(config, "sidebars_source", args.sidebars_source)
apply_path_override(config, "sidebars_output", args.sidebars_output)
apply_value_override(config, "base_url", args.base_url)
apply_package_overrides(config, args.package)
if args.extensions:
config.extensions = args.extensions


def build_parser() -> argparse.ArgumentParser:
"""Build and return the top-level argument parser with all subcommands registered."""
parser = argparse.ArgumentParser(prog="crocodocs")
parser = argparse.ArgumentParser(
prog="crocodocs",
description="CrocoDocs — documentation artifact generator for the Flet project. "
"Reads configuration from [tool.crocodocs] in pyproject.toml. "
"All path options are resolved relative to the working directory.",
)
subparsers = parser.add_subparsers(dest="command", required=True)

generate = subparsers.add_parser("generate")
generate.add_argument("--docs-path")
generate.add_argument("--manifest-output")
generate.add_argument("--output")
generate.add_argument("--sidebars-source")
generate.add_argument("--sidebars-output")
generate.add_argument("--base-url")
generate.add_argument("--package", action="append")
generate.add_argument("--extensions", action="append")
generate = subparsers.add_parser(
"generate",
help="Run a one-shot generation of all documentation artifacts.",
description="Generate all documentation artifacts: sidebars, docs manifest, "
"MDX partials, code examples, API data (via Griffe), and asset copies. "
"Defaults are loaded from [tool.crocodocs] in pyproject.toml; "
"CLI flags override individual settings.",
)
_add_shared_generate_arguments(generate)

watch = subparsers.add_parser(
"watch",
help="Watch source files and regenerate on changes, optionally running a child process.",
description="Run an initial generation, then watch source files for changes and "
"regenerate automatically. Optionally starts a child process (e.g. Docusaurus "
"dev server) that runs alongside the watcher. The watcher stops when the child "
"exits or when interrupted with Ctrl+C.",
)
_add_shared_generate_arguments(watch)
watch.add_argument(
"--interval",
type=float,
default=0.75,
metavar="SECS",
help="Maximum idle wait in seconds between child/debounce checks "
"(default: %(default)s).",
)
watch.add_argument(
"--debounce",
type=float,
default=0.5,
metavar="SECS",
help="Quiet period in seconds after the last detected change before "
"triggering regeneration. Prevents redundant rebuilds during multi-file "
"saves (default: %(default)s).",
)
watch.add_argument(
"--child-cwd",
metavar="PATH",
help="Working directory for the child command. "
"Resolved relative to the current working directory.",
)
watch.add_argument(
"watch_command",
nargs=argparse.REMAINDER,
metavar="COMMAND",
help="Command to run alongside the watcher (e.g. a dev server). "
"Use -- before the command to separate it from CrocoDocs options. "
"Example: crocodocs watch -- yarn exec docusaurus start",
)

return parser

Expand All @@ -44,15 +158,7 @@ def main(argv: list[str] | None = None) -> int:
config = load_config(project_root)

if args.command == "generate":
apply_path_override(config, "docs_path", args.docs_path)
apply_path_override(config, "manifest_output", args.manifest_output)
apply_path_override(config, "api_output", args.output)
apply_path_override(config, "sidebars_source", args.sidebars_source)
apply_path_override(config, "sidebars_output", args.sidebars_output)
apply_value_override(config, "base_url", args.base_url)
apply_package_overrides(config, args.package)
if args.extensions:
config.extensions = args.extensions
_apply_shared_generate_overrides(config, args)
run_generate(
config,
docs_path=config.docs_path,
Expand All @@ -62,5 +168,21 @@ def main(argv: list[str] | None = None) -> int:
)
return 0

if args.command == "watch":
_apply_shared_generate_overrides(config, args)
command = list(args.watch_command)
if command and command[0] == "--":
command = command[1:]
child_cwd = (
(project_root / args.child_cwd).resolve() if args.child_cwd else None
)
return run_watch(
config,
interval=args.interval,
debounce=args.debounce,
command=command or None,
command_cwd=child_cwd,
)

parser.error(f"Unsupported command: {args.command}")
return 2
Loading
Loading