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
4 changes: 4 additions & 0 deletions .github/workflows/check_outdated_dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ jobs:
frontend:
runs-on: ubuntu-latest
env:
# reflex-enterprise restricts `reflex run --env prod` to paid tiers but
# exempts reflex's own integration tests via the app harness flag.
APP_HARNESS_FLAG: "true"

steps:
- name: Checkout code
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/integration_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ jobs:

env:
REFLEX_WEB_WINDOWS_OVERRIDE: "1"
# reflex-enterprise restricts `reflex run --env prod` to paid tiers but
# exempts reflex's own integration tests via the app harness flag.
APP_HARNESS_FLAG: "true"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -220,6 +223,10 @@ jobs:
matrix:
# Note: py311 version chosen due to available arm64 darwin builds.
python-version: ["3.11", "3.12"]
env:
# reflex-enterprise restricts `reflex run --env prod` to paid tiers but
# exempts reflex's own integration tests via the app harness flag.
APP_HARNESS_FLAG: "true"
runs-on: macos-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ following command to generate the `CHANGELOG.md` file in each subpackage.
uv run towncrier build --config pyproject.toml --version v0.9.4
```

**Where changelogs are published:** the docs site renders every `CHANGELOG.md`
in the repo (repo root and `packages/*/`) under
[reflex.dev/docs/changelog/](https://reflex.dev/docs/changelog/). The
`reflex-enterprise` changelog is read from the installed `reflex-enterprise`
distribution at docs build time; it appears once the published wheel ships a
`CHANGELOG.md` and the docs app's lockfile picks up that version.

## ✅ Making a PR

Once you solve a current issue or improvement to Reflex, you can make a PR, and we will review the changes.
Expand Down
2 changes: 1 addition & 1 deletion docs/app/agent_files/_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def _section_for_path(url_path: Path) -> str:
return "Skills"
if path.startswith("ai/"):
return "AI Builder"
return _format_title(path.split("/", maxsplit=1)[0])
return _format_title(path.split("/", maxsplit=1)[0].removesuffix(".md"))


def _ordered_sections(
Expand Down
2 changes: 1 addition & 1 deletion docs/app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ dependencies = [
"python-frontmatter",
"reflex",
"reflex-docgen",
"reflex-enterprise",
"reflex-enterprise>=0.9.0.post1",
"reflex-hosting-cli",
"reflex-pyplot",
"reflex-integrations-docs",
Expand Down
111 changes: 111 additions & 0 deletions docs/app/reflex_docs/changelogs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Discovery of package changelogs surfaced on the docs site.

The monorepo packages manage their changelogs with towncrier: the main
``reflex`` changelog lives at the repo root and each subpackage ships its own
under ``packages/<name>/CHANGELOG.md``. The ``reflex-enterprise`` package is
developed in a separate repo, so its changelog is read from the installed
distribution instead (it only appears once the published wheel ships a
``CHANGELOG.md``).
"""

from importlib.metadata import PackageNotFoundError, distribution
from pathlib import Path

ENTERPRISE_PACKAGE = "reflex-enterprise"


def discover_repo_changelogs(repo_root: Path) -> dict[str, Path]:
"""Find the changelogs maintained in this repo.

Args:
repo_root: The repo checkout root (parent of the docs content tree).

Returns:
A mapping of package name to its CHANGELOG.md path — ``reflex`` for
the repo-root changelog plus one entry per subpackage that ships one.
"""
changelogs: dict[str, Path] = {}
root_changelog = repo_root / "CHANGELOG.md"
if root_changelog.is_file():
changelogs["reflex"] = root_changelog
for pkg_changelog in (repo_root / "packages").glob("*/CHANGELOG.md"):
changelogs[pkg_changelog.parent.name] = pkg_changelog
return changelogs


def find_distribution_changelog(package: str) -> Path | None:
"""Locate the CHANGELOG.md shipped with an installed distribution.

Args:
package: The distribution name (e.g. ``reflex-enterprise``).

Returns:
The path to the installed CHANGELOG.md, or None when the distribution
is not installed or does not ship one.
"""
try:
dist = distribution(package)
except PackageNotFoundError:
return None
candidates = [file for file in dist.files or () if file.name == "CHANGELOG.md"]
# Prefer the shallowest record — a wheel may also vendor third-party
# changelogs deeper in its tree.
for file in sorted(candidates, key=lambda file: len(file.parts)):
path = Path(str(dist.locate_file(file)))
if path.is_file():
return path
return None


def discover_changelogs(repo_root: Path) -> dict[str, Path]:
"""Find all package changelogs to publish on the docs site.

Args:
repo_root: The repo checkout root.

Returns:
A mapping of package name to CHANGELOG.md path, with ``reflex`` first
and the remaining packages in alphabetical order.
"""
changelogs = discover_repo_changelogs(repo_root)
enterprise_changelog = find_distribution_changelog(ENTERPRISE_PACKAGE)
if enterprise_changelog is not None:
changelogs[ENTERPRISE_PACKAGE] = enterprise_changelog
return {
name: changelogs[name]
for name in sorted(changelogs, key=lambda name: (name != "reflex", name))
}


def changelog_page_title(package: str) -> str:
"""Return the display title for a package changelog page.

Args:
package: The package name.

Returns:
The page title.
"""
return "Reflex Changelog" if package == "reflex" else f"{package} Changelog"
Comment thread
masenf marked this conversation as resolved.


def normalize_changelog(source: str, title: str) -> str:
"""Give changelog markdown a canonical top-level heading.

Towncrier-generated changelogs have no top-level heading, while
Keep-a-Changelog files (e.g. reflex-enterprise) start with a generic
``# Changelog``. Replace any existing H1 with *title* so every changelog
page renders consistently.

Args:
source: The raw changelog markdown.
title: The canonical page title.

Returns:
The normalized markdown.
"""
lines = source.lstrip().splitlines()
if lines and lines[0].startswith("# "):
del lines[0]
body = "\n".join(lines).strip("\n")
return f"# {title}\n\n{body}\n"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
15 changes: 15 additions & 0 deletions docs/app/reflex_docs/docgen_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,21 @@ def render_markdown(text: str) -> rx.Component:
return transformer.transform(doc)


def render_markdown_with_toc(text: str) -> tuple[list[tuple[int, str]], rx.Component]:
"""Render a plain markdown text string, also extracting its TOC headings.

Args:
text: The markdown source.

Returns:
A ``(toc, body)`` tuple where ``toc`` is a list of ``(level, text)``
heading tuples and ``body`` is the rendered component.
"""
doc = parse_document(text)
toc = [(h.level, _spans_to_plaintext(h.children)) for h in doc.headings]
return toc, ReflexDocTransformer().transform(doc)


def render_inline_markdown(text: str, class_name: str = "") -> rx.Component:
"""Render a short markdown string inline (links, code spans, emphasis).

Expand Down
50 changes: 49 additions & 1 deletion docs/app/reflex_docs/pages/docs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@
from reflex_pyplot import pyplot as pyplot
from reflex_site_shared.route import Route

from reflex_docs.docgen_pipeline import get_docgen_toc, render_docgen_document
from reflex_docs.changelogs import (
changelog_page_title,
discover_changelogs,
normalize_changelog,
)
from reflex_docs.docgen_pipeline import (
get_docgen_toc,
render_docgen_document,
render_markdown_with_toc,
)
from reflex_docs.pages.docs.component import multi_docs
from reflex_docs.pages.library_previews import components_previews_pages
from reflex_docs.templates.docpage import docpage
Expand Down Expand Up @@ -203,6 +212,26 @@ def make_docpage(route: str, title: str, doc_virtual: str, render_fn):
return docpage(set_path=route, t=title)(render_fn)


CHANGELOG_VIRTUAL_PREFIX = "docs/changelog/"


def handle_changelog_doc(doc: str, actual_path: str, resolved: ResolvedDoc):
"""Handle docs/changelog/** docs — package changelogs pulled from outside the docs tree.

Changelog markdown ships without a meaningful top-level heading, so the
canonical page title is normalized in and the table of contents is limited
to version headings.
"""

def comp(_actual=actual_path, _title=resolved.display_title):
source = normalize_changelog(Path(_actual).read_text(encoding="utf-8"), _title)
toc, body = render_markdown_with_toc(source)
toc = [(level, text) for level, text in toc if level <= 2]
return ((toc, source), body)

return make_docpage(resolved.route, resolved.display_title, doc, comp)


def handle_library_doc(
doc: str,
actual_path: str,
Expand Down Expand Up @@ -240,6 +269,9 @@ def get_component_docgen(virtual_doc: str, actual_path: str, title: str):
if virtual_doc.startswith("docs/library"):
return handle_library_doc(virtual_doc, actual_path, title, resolved)

if virtual_doc.startswith(CHANGELOG_VIRTUAL_PREFIX):
return handle_changelog_doc(virtual_doc, actual_path, resolved)

def comp(_actual=actual_path, _virtual=virtual_doc):
toc = get_docgen_toc(_actual)
doc_content = Path(_actual).read_text(encoding="utf-8")
Expand All @@ -253,6 +285,22 @@ def comp(_actual=actual_path, _virtual=virtual_doc):
return make_docpage(resolved.route, resolved.display_title, virtual_doc, comp)


# Package changelogs live outside the docs tree — the towncrier-managed ones
# at the repo root (CHANGELOG.md and packages/*/CHANGELOG.md) and the
# reflex-enterprise one inside the installed distribution. Reach up and pull
# them in as regular docs under docs/changelog/, with the main reflex
# changelog served at the section index.
changelog_packages: dict[str, str] = {} # package name → route
for _package, _changelog_path in discover_changelogs(_docs_dir.parent).items():
_virtual = (
f"{CHANGELOG_VIRTUAL_PREFIX}index.md"
if _package == "reflex"
else f"{CHANGELOG_VIRTUAL_PREFIX}{_package}.md"
)
all_docs[_virtual] = str(_changelog_path)
manual_titles[_virtual] = changelog_page_title(_package)
changelog_packages[_package] = doc_route_from_path(_virtual)

# Build doc_markdown_sources mapping
for _virtual, _actual in all_docs.items():
if _virtual.endswith("-style.md"):
Expand Down
4 changes: 1 addition & 3 deletions docs/app/reflex_docs/templates/docpage/docpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,7 @@ def docpage_footer(path: rx.Var[str]) -> rx.Component:
[
footer_link("Home", "/"),
footer_link("Blog", "/blog"),
footer_link(
"Changelog", "https://github.com/reflex-dev/reflex/releases"
),
footer_link("Changelog", "/changelog/"),
],
),
footer_link_flex(
Expand Down
15 changes: 13 additions & 2 deletions docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
)
from .sidebar_items.learn import backend, frontend, hosting, learn
from .sidebar_items.recipes import recipes
from .sidebar_items.reference import api_reference
from .sidebar_items.reference import api_reference, changelog_items
from .state import SideBarBase, SideBarItem

SIDEBAR_ICON_MAP = {
Expand Down Expand Up @@ -318,6 +318,7 @@ def append_to_items(items, flat_items):
+ mcp_items
+ skills_items
+ api_reference
+ changelog_items
+ enterprise_items,
flat_items,
)
Expand Down Expand Up @@ -451,6 +452,7 @@ def sidebar_comp(
html_lib_index: rx.vars.ArrayVar[list[int]],
graphing_libs_index: rx.vars.ArrayVar[list[int]],
api_reference_index: rx.vars.ArrayVar[list[int]],
changelog_index: rx.vars.ArrayVar[list[int]],
recipes_index: rx.vars.ArrayVar[list[int]],
enterprise_usage_index: rx.vars.ArrayVar[list[int]],
enterprise_component_index: rx.vars.ArrayVar[list[int]],
Expand Down Expand Up @@ -485,7 +487,7 @@ def sidebar_comp(
)

is_library = url.contains("library") | url.contains("/mcp-")
is_api_reference = url.contains("api-reference")
is_api_reference = url.contains("api-reference") | url.startswith("/changelog/")
is_enterprise = url.contains("enterprise")
is_default_docs = ~is_library & ~is_api_reference & ~is_enterprise

Expand Down Expand Up @@ -652,6 +654,14 @@ def sidebar_comp(
api_reference_index,
url,
),
create_sidebar_section(
"Changelog",
"/changelog/",
changelog_items,
changelog_index,
url,
connected_line=True,
),
class_name="m-0 p-0 flex flex-col items-start gap-8 w-full list-none list-style-none",
)
enterprise_content = rx.el.ul(
Expand Down Expand Up @@ -761,6 +771,7 @@ def sidebar(url=None, width: str = "100%") -> rx.Component:
html_lib_index=calculate_index(html_lib, normalized_url),
graphing_libs_index=calculate_index(graphing_libs, normalized_url),
api_reference_index=calculate_index(api_reference, normalized_url),
changelog_index=calculate_index(changelog_items, normalized_url),
recipes_index=calculate_index(recipes, normalized_url),
enterprise_usage_index=calculate_index(
enterprise_usage_items, normalized_url
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
from ..state import SideBarItem
from .item import create_item


def get_sidebar_items_changelog():
from reflex_docs.pages.docs import changelog_packages

return [
SideBarItem(names=package, link=route)
for package, route in changelog_packages.items()
]


def get_sidebar_items_api_reference():
from reflex_docs.pages.docs import api_reference, apiref

Expand All @@ -24,3 +34,4 @@ def get_sidebar_items_api_reference():


api_reference = get_sidebar_items_api_reference()
changelog_items = get_sidebar_items_changelog()
8 changes: 8 additions & 0 deletions docs/app/tests/test_agent_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,14 @@ def test_generate_dynamic_api_reference_files(monkeypatch):
)


def test_section_for_root_level_markdown_strips_extension():
"""Root-level docs (e.g. the changelog index) get a clean section name."""
from agent_files._plugin import _section_for_path

assert _section_for_path(Path("changelog.md")) == "Changelog"
assert _section_for_path(Path("changelog/reflex-base.md")) == "Changelog"


def test_generate_llms_full_txt_stitches_markdown_docs(monkeypatch, tmp_path):
"""llms-full.txt contains full Markdown page bodies with source URLs."""
monkeypatch.setattr(
Expand Down
Loading
Loading