diff --git a/CHANGELOG.md b/CHANGELOG.md index 839986f6c9..6d7bf771e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/client/pubspec.lock b/client/pubspec.lock index 9b4b200516..1c2465484d 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -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: diff --git a/client/web/flutter_bootstrap.js b/client/web/flutter_bootstrap.js index 86f1279767..c4128549d6 100644 --- a/client/web/flutter_bootstrap.js +++ b/client/web/flutter_bootstrap.js @@ -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; } diff --git a/client/web/index.html b/client/web/index.html index 0e630c5821..b4e7bed94f 100644 --- a/client/web/index.html +++ b/client/web/index.html @@ -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" } diff --git a/packages/flet/CHANGELOG.md b/packages/flet/CHANGELOG.md index 34080d3911..7b57fbbb5a 100644 --- a/packages/flet/CHANGELOG.md +++ b/packages/flet/CHANGELOG.md @@ -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. diff --git a/packages/flet/lib/src/utils/platform_utils_web.dart b/packages/flet/lib/src/utils/platform_utils_web.dart index 295f933e28..235d360650 100644 --- a/packages/flet/lib/src/utils/platform_utils_web.dart +++ b/packages/flet/lib/src/utils/platform_utils_web.dart @@ -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; diff --git a/packages/flet/pubspec.yaml b/packages/flet/pubspec.yaml index 98bb52183e..00c7d423e6 100644 --- a/packages/flet/pubspec.yaml +++ b/packages/flet/pubspec.yaml @@ -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: diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py index abfb010325..b653de3bc8 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py @@ -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. @@ -2530,16 +2528,37 @@ def package_python_app(self): # directory so it ships in `flutter build web` output. Cached # per-version under ~/.flet/pyodide// 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): """ @@ -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() @@ -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 diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py index b745fc647f..b6d58e24e3 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py @@ -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( @@ -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//). - 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//). + 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 @@ -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"), diff --git a/sdk/python/packages/flet-web/src/flet_web/patch_index.py b/sdk/python/packages/flet-web/src/flet_web/patch_index.py index 0e206cd5a3..878c62a68a 100644 --- a/sdk/python/packages/flet-web/src/flet_web/patch_index.py +++ b/sdk/python/packages/flet-web/src/flet_web/patch_index.py @@ -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}";') diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/flutter_bootstrap.js b/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/flutter_bootstrap.js index c852e5e1d2..23aa1b4d52 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/flutter_bootstrap.js +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/flutter_bootstrap.js @@ -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; } diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/index.html b/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/index.html index 891ee2f47d..6e55c4693f 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/index.html +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/index.html @@ -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" }