| A third target, and the seam that was already waiting for it f54ca45 nandi 2d ago | 1 | # `flutter-web` |
| 2 | |
| 3 | modal run .modal/flutter-web/container.py |
| 4 | just modal flutter-web |
| 5 | |
| 6 | Defined by `container.toml`; see `../spec.md` for the keys. |
| Three tarballs where a devShell was 5ce66d5 nandi 19h ago | 7 | Built on `debian:13-slim`. |
| 8 | |
| 9 | No nix, and that is the point of this container rather than an |
| 10 | incidental fact about it. The build is `tools/build-web.sh`, which |
| 11 | gets its Flutter, its JDK and its Clojure CLI from |
| 12 | `tools/toolchain.sh` -- three tarballs pinned by sha256 and unpacked |
| 13 | into a directory. So the image build is one `apt-get install` of |
| 14 | curl, git, rsync, tar and the two unarchivers, and everything that |
| 15 | used to happen before a line of Dart was compiled -- warming a |
| 16 | devShell, printing its environment, caching that against flake.lock, |
| 17 | copying a nix closure back to a volume afterwards -- does not happen |
| 18 | at all. The toolchain lands on the volume and the second run finds |
| 19 | it there. |
| 20 | |
| 21 | The other two Flutter targets keep their devShells: `apk` needs the |
| 22 | Android SDK and `flutter-desktop` needs GTK and a C++ toolchain, and |
| 23 | a host toolchain is what nix is better at than a tarball. The web |
| 24 | target needs a Dart and a JVM, which is what a tarball is for. |
| 25 | |
| 26 | Same incremental shape as before -- the working tree, the generated |
| 27 | Dart under `flutter/lib/cljd-out` and Flutter's caches live on the |
| 28 | `devshell` volume, under `frq-flutter-web/` so the desktop |
| 29 | container's directory beside it is untouched. None of them is copied |
| 30 | in from the laptop: a checkout's copy of the compiler's output is |
| 31 | not this container's, and overwriting the volume's with it is how an |
| 32 | incremental build stops being one. |
| 33 | |
| 34 | One build mode, not two. A `fast` mode (dart2js -O1, no icon |
| 35 | tree-shaking, no service worker) measured 52.5s against the release |
| 36 | build's 49.8s on the same source change, so what it bought was a |
| 37 | bigger bundle. `--no-wasm-dry-run` is the flag that did pay, and it |
| 38 | is in the one build there is. |
| A third target, and the seam that was already waiting for it f54ca45 nandi 2d ago | 39 | |
| 40 | Runs as a Sandbox on a real VM (kernel 6.x, not gVisor). The command |
| 41 | is the sandbox's own process, so it dies when the command exits. |
| 42 | |
| 43 | To look at what it built, ask for the serve action -- `[network] |
| 44 | ports` tunnels 8080 out, and the URL is printed once the sandbox is |
| 45 | scheduled: |
| 46 | |
| 47 | modal run .modal/flutter-web/container.py \ |
| Three tarballs where a devShell was 5ce66d5 nandi 19h ago | 48 | --command 'cd /devshell/frq-flutter-web && tools/build-web.sh serve 8080' |
| A third target, and the seam that was already waiting for it f54ca45 nandi 2d ago | 49 | |
| 50 | That blocks until you Ctrl-C it, and it bills until you do. |
| 51 | |
| 52 | ## It compiles; it does not start |
| 53 | |
| 54 | dart2js links the whole app -- `main.dart.js` is 3.6MB and has |
| 55 | `frq`, `irc`, `atproto` and `handshake` all through it, so the |
| 56 | `dart:io` imports under `flutter/src` are not the wall they look |
| 57 | like. What stops it is the first line of `main`: |
| 58 | `getApplicationDocumentsDirectory` is a platform channel, path_provider |
| 59 | ships no web implementation, and the channel with no handler behind it |
| 60 | throws `MissingPluginException`. `main` awaits that before installing |
| 61 | `frq.io.dart`, so no widget is ever built and the page stays white. |
| 62 | |
| 63 | That is the seam doing its job rather than a build problem. The fix is |
| 64 | a `frq.io.web` behind `frq.io` -- the browser's answer for a private |
| 65 | file is IndexedDB or localStorage, not a directory -- and `main` |
| 66 | choosing it the way `flutter/src/frq/main.cljd` chooses the Dart one |
| 67 | now. `frq.net.web` is the next one after it, for the same reason: a |
| 68 | browser has no raw socket, so the IRC connection wants a WebSocket. |
| 69 | |
| 70 | ## Serving it |
| 71 | |
| 72 | `serve.py` is the other half: a Function that mounts the same volume |
| 73 | and hands out `flutter/build/web`, so a rebuild in the Sandbox is |
| 74 | picked up by the next cold start with nothing redeployed. |
| 75 | |
| 76 | modal deploy .modal/flutter-web/serve.py |
| 77 | |
| 78 | `[network] ports` tunnels 8080 out of the Sandbox as well, for the |
| 79 | case where you want the build and the server to be one process. |