Skip to content

Commit 8c8307c

Browse files
committed
poc
1 parent 2a56caa commit 8c8307c

File tree

10 files changed

+185
-30
lines changed

10 files changed

+185
-30
lines changed

sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import tempfile
77
from pathlib import Path
88

9-
from flet.controls.types import WebRenderer
9+
from flet.controls.types import RouteUrlStrategy, WebRenderer
1010
from flet.utils import copy_tree, is_within_directory, random_string
1111
from flet_cli.commands.base import BaseCommand
1212
from flet_cli.utils.project_dependencies import (
@@ -303,7 +303,7 @@ def filter_tar(tarinfo: tarfile.TarInfo):
303303
or get_pyproject("tool.flet.web.renderer")
304304
or "auto"
305305
),
306-
route_url_strategy=str(
306+
route_url_strategy=RouteUrlStrategy(
307307
options.route_url_strategy
308308
or get_pyproject("tool.flet.web.route_url_strategy")
309309
or "path"

sdk/python/packages/flet-web/src/flet_web/patch_index.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
from flet.controls.types import RouteUrlStrategy, WebRenderer
77

88

9+
def _replace_flet_value(index: str, key: str, value: str) -> str:
10+
"""
11+
Replace a single property inside the `var flet = { ... }` block.
12+
"""
13+
pattern = rf"({re.escape(key)}\s*:\s*)([^,}}]+)"
14+
replacement = rf"\1{value}"
15+
return re.sub(pattern, replacement, index, flags=re.MULTILINE)
16+
17+
18+
def _normalize_base(base_href: str) -> str:
19+
base_url = base_href.strip("/").strip() if base_href else ""
20+
return "/" if base_url == "" else f"/{base_url}/"
21+
22+
923
def patch_index_html(
1024
index_path: str,
1125
base_href: str,
@@ -22,33 +36,44 @@ def patch_index_html(
2236
with open(index_path, encoding="utf-8") as f:
2337
index = f.read()
2438

39+
base = _normalize_base(base_href)
40+
if base_href:
41+
index = index.replace('<base href="/">', f'<base href="{base}">')
42+
2543
app_config = []
2644

2745
if pyodide and pyodide_script_path:
2846
module_name = Path(pyodide_script_path).stem
2947
app_config.append("flet.pyodide = true;")
3048
app_config.append(f"flet.micropipIncludePre = {str(pyodide_pre).lower()};")
31-
app_config.append(f'flet.pythonModuleName = "{module_name}";')
49+
app_config.append(f"flet.pythonModuleName = {module_name!r};")
3250

33-
app_config.append(f"flet.noCdn={str(no_cdn).lower()};")
34-
app_config.append(f'flet.webRenderer="{web_renderer.value}";')
35-
app_config.append(
36-
f'flet.routeUrlStrategy="{route_url_strategy if isinstance(route_url_strategy, str) else route_url_strategy.value}";'
37-
)
51+
app_config.append(f"flet.noCdn = {str(no_cdn).lower()};")
52+
app_config.append(f"flet.webRenderer = {web_renderer.value!r};")
53+
app_config.append(f"flet.routeUrlStrategy = {route_url_strategy.value!r};")
3854

3955
if websocket_endpoint_path:
40-
app_config.append(f'flet.webSocketEndpoint="{websocket_endpoint_path}";')
56+
app_config.append(f"flet.webSocketEndpoint={websocket_endpoint_path!r};")
4157

4258
index = index.replace(
4359
"<!-- fletAppConfig -->",
4460
"<script>\n{}\n</script>".format("\n".join(app_config)),
4561
)
4662

47-
if base_href:
48-
base_url = base_href.strip("/").strip()
49-
index = index.replace(
50-
'<base href="/">',
51-
'<base href="{}">'.format("/" if base_url == "" else f"/{base_url}/"),
63+
# Update flet bootstrap object to respect base-url and routing options.
64+
index = _replace_flet_value(index, "pyodide", str(pyodide).lower())
65+
index = _replace_flet_value(index, "noCdn", str(no_cdn).lower())
66+
index = _replace_flet_value(index, "webRenderer", f"{web_renderer.value!r}")
67+
index = _replace_flet_value(
68+
index, "routeUrlStrategy", f"{route_url_strategy.value!r}"
69+
)
70+
index = _replace_flet_value(index, "entrypointBaseUrl", f"{base!r}")
71+
index = _replace_flet_value(index, "assetBase", f"{base!r}")
72+
index = _replace_flet_value(index, "canvasKitBaseUrl", f"'{base}canvaskit/'")
73+
index = _replace_flet_value(index, "pyodideUrl", f"'{base}pyodide/pyodide.js'")
74+
if websocket_endpoint_path:
75+
index = _replace_flet_value(
76+
index, "webSocketEndpoint", f"{websocket_endpoint_path!r}"
5277
)
5378

5479
if app_name:

sdk/python/packages/flet/docs/apps/examples-gallery/requirements.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

sdk/python/packages/flet/docs/controls/checkbox.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,22 @@ example_images: ../test-images/examples/material/golden/macos/checkbox
1414
--8<-- "{{ examples }}/basic.py"
1515
```
1616

17-
{{ iframe(route="checkbox/basic", height="400") }}
17+
{{ demo("checkbox/basic", height="400", width="80%") }}
1818

1919
### Handling events
2020

2121
```python
2222
--8<-- "{{ examples }}/handling_events.py"
2323
```
2424

25-
{{ iframe(route="checkbox/handling_events", height="520") }}
25+
{{ demo("checkbox/handling_events", height="200", width="80%") }}
2626

2727
### Styled checkboxes
2828

2929
```python
3030
--8<-- "{{ examples }}/styled.py"
3131
```
3232

33-
{{ iframe(route="checkbox/styled", height="520") }}
33+
{{ demo("checkbox/styled", height="200", width="80%") }}
3434

3535
{{ class_members(class_name) }}

sdk/python/packages/flet/docs/extras/macros/__init__.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from typing import Optional
23
from urllib.parse import urlparse
34

45
from .cli_to_md import render_flet_cli_as_markdown
@@ -128,13 +129,16 @@ def flet_cli_as_markdown(command: str = "", subcommands_only: bool = True):
128129
command=command, subcommands_only=subcommands_only
129130
)
130131

132+
@env.macro
133+
def controls_overview():
134+
return render_controls_overview()
135+
131136
@env.macro
132137
def iframe(
133138
src=None,
134139
*,
135140
route=None,
136141
base="/apps/examples-gallery/dist/index.html#/",
137-
# base="http://127.0.0.1:3000/",
138142
width="100%",
139143
height="480",
140144
title=None,
@@ -153,5 +157,20 @@ def iframe(
153157
)
154158

155159
@env.macro
156-
def controls_overview():
157-
return render_controls_overview()
160+
def demo(
161+
route: str,
162+
*,
163+
width: str = "100%",
164+
height: str = "350",
165+
title: Optional[str] = None,
166+
):
167+
"""
168+
Embed an examples gallery route as a centered demo iframe.
169+
"""
170+
return render_iframe(
171+
route=route,
172+
base="/apps/examples-gallery/dist/index.html#/",
173+
width=width,
174+
height=height,
175+
title=title,
176+
)

sdk/python/packages/flet/docs/extras/macros/iframe.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
from urllib.parse import urlparse
44

55
# Default iframe base can be overridden for local dev via FLET_IFRAME_BASE.
6-
DEFAULT_IFRAME_BASE = os.environ.get("FLET_IFRAME_BASE", "/apps/dist/index.html#/")
6+
# Points to the published examples gallery bundled with the docs.
7+
DEFAULT_IFRAME_BASE = os.environ.get(
8+
"FLET_IFRAME_BASE", "/apps/examples-gallery/dist/index.html#/"
9+
)
710

811

912
def _prettify_token(token: str) -> str:
@@ -39,7 +42,7 @@ def render_iframe(
3942
4043
When route is provided, the iframe source is constructed from the base URL.
4144
The base defaults to the published examples gallery served by MkDocs:
42-
/assets/dist/index.html#/ (override with FLET_IFRAME_BASE).
45+
/apps/examples-gallery/dist/index.html#/ (override with FLET_IFRAME_BASE).
4346
"""
4447
base = base or DEFAULT_IFRAME_BASE
4548

@@ -56,12 +59,14 @@ def render_iframe(
5659
raise ValueError("Either src or route must be provided")
5760

5861
if title is None:
59-
parsed = urlparse(src)
60-
candidate = parsed.path or src
61-
if "." not in candidate:
62-
title = _pretty_from_route(route or candidate)
62+
# Prefer a human-friendly title from the route when available, even if
63+
# the underlying src includes an index.html with a fragment.
64+
if route:
65+
title = _pretty_from_route(route)
6366
else:
64-
title = "iframe"
67+
parsed = urlparse(src)
68+
candidate = parsed.path or src
69+
title = _pretty_from_route(candidate) if "." not in candidate else "iframe"
6570

6671
attrs = [
6772
f'src="{src}"',
@@ -73,4 +78,6 @@ def render_iframe(
7378
]
7479
if allow:
7580
attrs.append(f'allow="{allow}"')
76-
return f"<iframe {' '.join(attrs)}></iframe>"
81+
iframe_html = f"<iframe {' '.join(attrs)}></iframe>"
82+
83+
return f'<div style="display:flex; justify-content:center;">{iframe_html}</div>'

sdk/python/packages/flet/mkdocs.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ watch:
9696
- ../../../../LICENSE
9797
- ../../../../README.md
9898

99-
10099
# Plugins
101100
plugins:
102101
# - footnotes
@@ -163,6 +162,10 @@ plugins:
163162
- source_dir: integration_tests
164163
target_url_path: test-images
165164
include_exts: [".png", ".gif", ".svg"]
165+
- examples_gallery:
166+
src: apps/examples-gallery/src/main.py
167+
dist: apps/examples-gallery/dist
168+
base_url: apps/examples-gallery/dist
166169

167170
# Markdown Extensions
168171
markdown_extensions:

sdk/python/packages/flet/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ web = ["flet-web"]
3131
[project.scripts]
3232
flet = "flet.cli:main"
3333

34+
[project.entry-points."mkdocs.plugins"]
35+
examples_gallery = "flet.docs_plugins.examples_gallery:ExamplesGalleryPlugin"
36+
3437
[dependency-groups]
3538
extensions = [
3639
"flet-ads",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# MkDocs integration helpers (e.g., examples gallery plugin).
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import os
2+
import subprocess
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
from mkdocs.config import config_options
7+
from mkdocs.config.defaults import MkDocsConfig
8+
from mkdocs.plugins import BasePlugin
9+
10+
11+
def _latest_mtime(root: Path) -> float:
12+
"""Return the latest modification time for all files under a directory."""
13+
mtimes = []
14+
for path in root.rglob("*"):
15+
if path.is_file():
16+
mtimes.append(path.stat().st_mtime)
17+
return max(mtimes) if mtimes else 0.0
18+
19+
20+
class ExamplesGalleryPlugin(BasePlugin):
21+
"""Build the examples gallery Flet app before MkDocs runs.
22+
23+
This plugin:
24+
- Finds the examples gallery entry point (default: docs/apps/examples-gallery/src/main.py).
25+
- Runs `flet publish` (via `uv run --active`) into the configured dist folder.
26+
- Skips the build when the dist/index.html is newer than the source tree.
27+
28+
Env opts:
29+
- FLET_SKIP_EXAMPLES_GALLERY=1 — skip building (useful for fast local docs iteration).
30+
31+
Config options (mkdocs.yml):
32+
- enabled: bool (default True)
33+
- src: path to the gallery main.py relative to docs_dir
34+
- dist: output folder relative to docs_dir
35+
- base_url: base URL passed to `--base-url`
36+
- command: optional custom command list to run instead of the default
37+
- env: optional extra env vars to inject into the command
38+
"""
39+
40+
config_scheme = (
41+
("enabled", config_options.Type(bool, default=True)),
42+
("src", config_options.Type(str, default="apps/examples-gallery/src/main.py")),
43+
("dist", config_options.Type(str, default="apps/examples-gallery/dist")),
44+
("base_url", config_options.Type(str, default="apps/examples-gallery/dist")),
45+
("command", config_options.Type(list, default=None)),
46+
("env", config_options.Type(dict, default=None)),
47+
)
48+
49+
def on_pre_build(self, config: MkDocsConfig) -> None:
50+
"""
51+
Optionally publish the examples gallery before the docs build.
52+
53+
- Honours `enabled` and `FLET_SKIP_EXAMPLES_GALLERY`.
54+
- Skips when the existing dist/index.html is newer than the source tree.
55+
- Runs the configured command (or the default `uv run --active flet publish ...`)
56+
with `docs_dir` as the working directory and merged env vars.
57+
"""
58+
if not self.config.get("enabled", True):
59+
return
60+
if os.environ.get("FLET_SKIP_EXAMPLES_GALLERY"):
61+
return
62+
63+
docs_dir = Path(config["docs_dir"])
64+
src = docs_dir / self.config["src"]
65+
dist_dir = docs_dir / self.config["dist"]
66+
dist_index = dist_dir / "index.html"
67+
68+
if not src.exists():
69+
return
70+
71+
src_mtime = _latest_mtime(src.parent)
72+
dist_mtime = dist_index.stat().st_mtime if dist_index.exists() else 0.0
73+
if dist_mtime >= src_mtime:
74+
return
75+
76+
cmd: Optional[list[str]] = self.config.get("command")
77+
if not cmd:
78+
cmd = [
79+
"uv",
80+
"run",
81+
"--active",
82+
"flet",
83+
"publish",
84+
str(src),
85+
"--distpath",
86+
str(dist_dir),
87+
"--base-url",
88+
self.config["base_url"],
89+
"--route-url-strategy",
90+
"hash",
91+
"--pre",
92+
]
93+
94+
env: dict[str, str] = dict(os.environ)
95+
extra_env = self.config.get("env") or {}
96+
env.update({k: str(v) for k, v in extra_env.items()})
97+
98+
subprocess.run(cmd, cwd=docs_dir, check=True, env=env)

0 commit comments

Comments
 (0)