Skip to content
Merged
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
## 0.86.7

### Bug fixes

* Fix `flet build web --no-cdn` still loading Pyodide from jsdelivr. The web template chose between the bundled runtime and the CDN with `{% if cookiecutter.no_cdn == "True" %}`, but `flet build` passes `no_cdn` through cookiecutter's `extra_context` as a Python `bool`, which Jinja never renders to a string before comparing — `True == "True"` is `False`, so the CDN branch was taken in *both* modes and `--no-cdn` builds downloaded, cached and shipped ~15 MB of Pyodide that the browser then ignored. Only `pyodideUrl` was affected: `flet.noCdn` itself is derived separately, via `"{{ cookiecutter.no_cdn }}".toLowerCase() == "true"`, which does render correctly — which is why CanvasKit and the fallback fonts loaded locally as expected while Pyodide alone went to the CDN, making the failure look like a Pyodide-specific quirk rather than a template bug. The template's other `no_cdn` test (`assets/FontManifest.json`) already used plain truthiness; all conditions now do, and no string comparisons against `"True"` remain by @FeodorFitsner.

* Fix the bundled Pyodide URL being origin-absolute in `--no-cdn` builds, so a sub-path deployment (`flet build web --base-url myapp`) requested `/pyodide/pyodide.mjs` and got a 404 while the file sat at `/myapp/pyodide/pyodide.mjs`. It now renders relative to the configured base URL, as `canvasKitBaseUrl` does. Builds without `--base-url` render exactly the same URL as before by @FeodorFitsner.

### Improvements

* `flet build web` and `flet publish` no longer bundle CanvasKit and Pyodide when CDN mode is on (the default), taking a minimal web build from **71 MB to 19 MB**. In CDN mode Flutter loads CanvasKit from `gstatic.com` and Flet points `pyodideUrl` at jsdelivr, so both copies were dead weight the browser never requested — yet `flutter build web` always emits `canvaskit/` (~37 MB), and `ensure_pyodide()` ran unconditionally in both commands, downloading and copying a further ~15 MB. Neither is fetched, so nothing about how a CDN-mode app loads changes; verified with a network log showing the built app pulling `chromium/canvaskit.{js,wasm}` from gstatic and the full Pyodide runtime from jsdelivr, with no request to a local `canvaskit/` or `pyodide/` path. `--no-cdn` (or `[tool.flet.web] cdn = false`) still bundles everything and is unchanged. `flet build` also clears a `pyodide/` left in the reused Flutter project by an earlier `--no-cdn` build, so switching modes doesn't silently keep shipping it by @FeodorFitsner.

* `flet.canvasKitBaseUrl` and `flet.fontFallbackBaseUrl` are now honored whenever they are set, instead of only when `flet.noCdn` is true. Previously `flutter_bootstrap.js` applied both inside `if (flet.noCdn)`, so a host serving its own copy of the runtime — a CDN-restricted network, an air-gapped deployment, or a platform that mirrors the runtime on its own origin — had to also set `noCdn` for the assignment to take effect at all, and assigning the URL alone failed silently by booting off gstatic anyway. Where a runtime asset is fetched from is now independent of what the build bundled: both values default to `null` in CDN mode and are pinned by `flet build`/`patch_index.py` only when bundling by @FeodorFitsner.

## 0.86.6

### Bug fixes
Expand Down
2 changes: 1 addition & 1 deletion client/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ packages:
path: "../packages/flet"
relative: true
source: path
version: "0.86.6"
version: "0.86.7"
flet_ads:
dependency: "direct main"
description:
Expand Down
8 changes: 7 additions & 1 deletion client/web/flutter_bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ var flutterConfig = {
if (flet.webRenderer != "auto") {
flutterConfig.renderer = flet.webRenderer;
}
if (flet.noCdn) {
// Keyed off the values themselves, not off `flet.noCdn`: a host serving its
// own copy of the runtime can point these anywhere without pretending the app
// was built with `--no-cdn`. Left unset, Flutter falls back to gstatic for
// CanvasKit and to Google Fonts for the Noto fallbacks.
if (flet.canvasKitBaseUrl) {
flutterConfig.canvasKitBaseUrl = flet.canvasKitBaseUrl;
}
if (flet.fontFallbackBaseUrl) {
flutterConfig.fontFallbackBaseUrl = flet.fontFallbackBaseUrl;
}

Expand Down
8 changes: 6 additions & 2 deletions client/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,18 @@
entrypointBaseUrl: "/",
assetBase: "/",
routeUrlStrategy: "path",
canvasKitBaseUrl: "/canvaskit/",
// Left unset so Flutter resolves CanvasKit and the Noto fallback fonts
// from their CDNs. `patch_index.py` fills both in with local paths when
// building with `--no-cdn`, and a host serving its own copy of the
// runtime can assign them without also claiming a no-CDN build.
canvasKitBaseUrl: null,
// Default fallback only — `patch_index.py` overrides this with the
// resolved per-build URL (CDN or local) at deploy time. The `.mjs`
// suffix is required because python-worker.js is a module worker
// that loads the runtime via dynamic `import()`. See client/web/python.js.
pyodideUrl: "/pyodide/pyodide.mjs",
webRenderer: "auto",
fontFallbackBaseUrl: "assets/fonts/", // for Noto Emoji, use Google CDN
fontFallbackBaseUrl: null,
appPackageUrl: "app.tar.gz"
}

Expand Down
4 changes: 4 additions & 0 deletions packages/flet/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.86.7

* `FletJS.canvasKitBaseUrl` is now `String?`. `flutter_bootstrap.js` applies `flet.canvasKitBaseUrl` and `flet.fontFallbackBaseUrl` whenever they are set rather than only when `flet.noCdn` is true, and both default to `null` in CDN mode — so a host serving its own copy of the runtime can point them anywhere without also claiming a no-CDN build. The getter has no readers in this package; the annotation now matches the value it can carry.

## 0.86.6

* Fix `Container` applying its `padding` twice, and confining ink/hover effects to the size of its content, when `ink` is enabled together with `animate`. `padding` and `alignment` were passed both to the outer `AnimatedContainer` and to the inner `Container` under the `InkWell`, so the effective padding doubled; the duplicated `alignment` also made the `Material`/`InkWell` shrink-wrap to the content, so splashes and the hover overlay no longer reached the container's edges even though `bgcolor` filled it. Both properties now live only on the inner container - an `AnimatedContainer` when `animate` is set, so they still animate - matching the non-animated ink path.
Expand Down
2 changes: 1 addition & 1 deletion packages/flet/lib/src/utils/platform_utils_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ extension FletJSExtension on FletJS {
external bool get noCdn;
external String get webSocketEndpoint;
external String get routeUrlStrategy;
external String get canvasKitBaseUrl;
external String? get canvasKitBaseUrl;
external String get pyodideUrl;
external String get webRenderer;
external String? get appPackageUrl;
Expand Down
2 changes: 1 addition & 1 deletion packages/flet/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: flet
description: Write entire Flutter app in Python or add server-driven UI experience into existing Flutter app.
homepage: https://flet.dev
repository: https://github.com/flet-dev/flet/tree/main/packages/flet
version: 0.86.6
version: 0.86.7

# Supported platforms
platforms:
Expand Down
71 changes: 57 additions & 14 deletions sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1381,9 +1381,7 @@ def _xml_attr_value(v):
self.options.no_wasm
or self.get_pyproject("tool.flet.web.wasm") == False # noqa: E712
),
"no_cdn": (
self.options.no_cdn or self.get_pyproject("tool.flet.web.cdn") == False # noqa: E712
),
"no_cdn": self.resolve_no_cdn(),
# Surface the resolved Pyodide release to the cookiecutter
# context so the web template's index.html can wire the
# correct jsdelivr URL when CDN mode is on.
Expand Down Expand Up @@ -2530,16 +2528,37 @@ def package_python_app(self):
# directory so it ships in `flutter build web` output. Cached
# per-version under ~/.flet/pyodide/<version>/ so subsequent builds
# are no-ops.
#
# Skipped in CDN mode: `patch_index.py` then points `flet.pyodideUrl`
# at jsdelivr, so a bundled copy would add ~15 MB to the build that
# the browser never requests.
if self.package_platform == "Emscripten":
from flet_cli.utils.pyodide import ensure_pyodide

self.update_status("[bold blue]Preparing Pyodide runtime...")
pyodide_dest = self.flutter_dir / "web" / "pyodide"
ensure_pyodide(self.python_release.pyodide, pyodide_dest)
console.log(
f"Pyodide {self.python_release.pyodide} ready "
f"{self.emojis['checkmark']}"
)
if self.resolve_no_cdn():
from flet_cli.utils.pyodide import ensure_pyodide

self.update_status("[bold blue]Preparing Pyodide runtime...")
ensure_pyodide(self.python_release.pyodide, pyodide_dest)
console.log(
f"Pyodide {self.python_release.pyodide} ready "
f"{self.emojis['checkmark']}"
)
elif pyodide_dest.exists():
# The Flutter project is reused across builds, so a copy left
# by an earlier `--no-cdn` build would otherwise still ship.
shutil.rmtree(pyodide_dest, ignore_errors=True)

def resolve_no_cdn(self) -> bool:
"""
Whether CanvasKit, Pyodide and fallback fonts should be bundled with
the app instead of loaded from their CDNs.

Returns:
True when `--no-cdn` was passed or `tool.flet.web.cdn` is `false`.
"""
return bool(
self.options.no_cdn or self.get_pyproject("tool.flet.web.cdn") == False # noqa: E712
)

def get_bool_setting(self, cli_option, pyproj_setting, default_value):
"""
Expand Down Expand Up @@ -2834,9 +2853,11 @@ def ignore(path, names):
ignore=make_ignore_fn(build_output_dir, build_output_glob),
)

if self.target_platform == "web" and self.assets_path.exists():
# copy `assets` directory contents to the output directory
copy_tree(str(self.assets_path), str(self.out_dir))
if self.target_platform == "web":
self.prune_cdn_assets()
if self.assets_path.exists():
# copy `assets` directory contents to the output directory
copy_tree(str(self.assets_path), str(self.out_dir))
elif self.target_platform in {"apk", "aab"}:
self.rename_android_build_outputs()

Expand All @@ -2845,6 +2866,28 @@ def ignore(path, names):
f"directory {self.emojis['checkmark']}"
)

def prune_cdn_assets(self):
"""
Remove `canvaskit/` from a CDN-mode web build.

`flutter build web` always emits CanvasKit, but in CDN mode the web
template leaves `flet.canvasKitBaseUrl` unset, so Flutter loads it
from gstatic and these ~37 MB are never requested.
"""
if self.resolve_no_cdn():
return

canvaskit_dir = self.out_dir / "canvaskit"
if not canvaskit_dir.exists():
return

shutil.rmtree(canvaskit_dir, ignore_errors=True)
if self.verbose > 0:
console.log(
"Removed canvaskit/ from build output (loaded from CDN)",
style=verbose1_style,
)

def rename_android_build_outputs(self):
"""
Rename copied Android release artifacts so they honor user-configured
Expand Down
22 changes: 16 additions & 6 deletions sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ def handle(self, options: argparse.Namespace) -> None:
sys.exit(2)
print(f"Using Python {python_release.short} (Pyodide {python_release.pyodide})")

# Resolved here rather than at first use: it decides which runtime
# assets get copied into `dist`, not just how index.html is patched.
no_cdn = options.no_cdn or get_pyproject("tool.flet.web.cdn") == False # noqa: E712

if get_pyproject("tool.flet.app.path"):
script_dir = script_dir.joinpath(get_pyproject("tool.flet.app.path"))
script_path = script_dir.joinpath(
Expand All @@ -225,10 +229,18 @@ def handle(self, options: argparse.Namespace) -> None:
sys.exit(1)
copy_tree(web_path, dist_dir)

# Drop in the Pyodide runtime that matches the resolved Python version
# (cached under ~/.flet/pyodide/<version>/).
print(f"Preparing Pyodide {python_release.pyodide} runtime...")
ensure_pyodide(python_release.pyodide, Path(dist_dir) / "pyodide")
if no_cdn:
# Drop in the Pyodide runtime that matches the resolved Python
# version (cached under ~/.flet/pyodide/<version>/).
print(f"Preparing Pyodide {python_release.pyodide} runtime...")
ensure_pyodide(python_release.pyodide, Path(dist_dir) / "pyodide")
else:
# CDN mode: `patch_index_html` points `flet.pyodideUrl` at
# jsdelivr and `flutter_bootstrap.js` loads CanvasKit from
# gstatic, so neither copy the `web` package ships is ever
# requested. Dropping them takes ~52 MB off `dist`.
for cdn_asset in ("pyodide", "canvaskit"):
shutil.rmtree(Path(dist_dir) / cdn_asset, ignore_errors=True)

# copy assets
assets_dir = options.assets_dir
Expand Down Expand Up @@ -343,8 +355,6 @@ def filter_tar(tarinfo: tarfile.TarInfo):
"tool.flet.web.pwa_theme_color"
)

no_cdn = options.no_cdn or get_pyproject("tool.flet.web.cdn") == False # noqa: E712

print("Patching index.html")
patch_index_html(
index_path=os.path.join(dist_dir, "index.html"),
Expand Down
7 changes: 7 additions & 0 deletions sdk/python/packages/flet-web/src/flet_web/patch_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ def patch_index_html(
)
app_config.append(f'flet.pyodideUrl="{pyodide_url}";')

# Only pinned when bundling: left unset, `flutter_bootstrap.js` lets
# Flutter resolve CanvasKit from gstatic and the Noto fallback fonts from
# Google Fonts, and `flet build`/`flet publish` skip shipping local copies.
if no_cdn:
app_config.append(f'flet.canvasKitBaseUrl="{base_url}canvaskit/";')
app_config.append('flet.fontFallbackBaseUrl="assets/fonts/";')

app_config.append(f"flet.noCdn={str(no_cdn).lower()};")
app_config.append(f'flet.webRenderer="{web_renderer.value}";')
app_config.append(f'flet.routeUrlStrategy="{route_url_strategy.value}";')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ var flutterConfig = {
if (flet.webRenderer != "auto") {
flutterConfig.renderer = flet.webRenderer;
}
if (flet.noCdn) {
// Keyed off the values themselves, not off `flet.noCdn`: a host serving its
// own copy of the runtime can point these anywhere without pretending the app
// was built with `--no-cdn`. Left unset, Flutter falls back to gstatic for
// CanvasKit and to Google Fonts for the Noto fallbacks.
if (flet.canvasKitBaseUrl) {
flutterConfig.canvasKitBaseUrl = flet.canvasKitBaseUrl;
}
if (flet.fontFallbackBaseUrl) {
flutterConfig.fontFallbackBaseUrl = flet.fontFallbackBaseUrl;
}

Expand Down
20 changes: 16 additions & 4 deletions sdk/python/templates/build/{{cookiecutter.out_dir}}/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,32 @@
entrypointBaseUrl: "{{ cookiecutter.base_url }}",
assetBase: "{{ cookiecutter.base_url }}",
routeUrlStrategy: "{{ cookiecutter.route_url_strategy }}",
canvasKitBaseUrl: "/canvaskit/",
// Only pinned when bundling. Left null, Flutter resolves CanvasKit from
// gstatic and the Noto fallback fonts from Google Fonts, and the build
// skips shipping local copies. A host serving its own runtime can assign
// these without also claiming a no-CDN build.
{% if cookiecutter.no_cdn %}
canvasKitBaseUrl: "{{ cookiecutter.base_url }}canvaskit/",
{% else %}
canvasKitBaseUrl: null,
{% endif %}
// .mjs is required: python-worker.js is a module worker and loads
// the runtime via dynamic `import()`. Pyodide >= 0.29 also refuses
// to run in classic workers. Use the local copy that `flet build`
// drops into web/pyodide/ when --no-cdn is set; otherwise pull
// from jsdelivr so the build artifact can stay slim.
{% if cookiecutter.no_cdn == "True" %}
pyodideUrl: "/pyodide/pyodide.mjs",
{% if cookiecutter.no_cdn %}
pyodideUrl: "{{ cookiecutter.base_url }}pyodide/pyodide.mjs",
{% else %}
pyodideUrl: "https://cdn.jsdelivr.net/pyodide/v{{ cookiecutter.pyodide_version }}/full/pyodide.mjs",
{% endif %}
pythonModuleName: "{{ cookiecutter.python_module_name }}",
webRenderer: "{{ cookiecutter.web_renderer }}",
fontFallbackBaseUrl: "assets/fonts/", // for Noto Emoji, use Google CDN
{% if cookiecutter.no_cdn %}
fontFallbackBaseUrl: "assets/fonts/", // Noto Emoji, bundled
{% else %}
fontFallbackBaseUrl: null, // Noto Emoji from Google Fonts
{% endif %}
appPackageUrl: "assets/app/app.zip"
}

Expand Down