Release 0.4.1: ES module support, dioxus-native 0.7.3, recovered tests - #83
Merged
Conversation
added 8 commits
August 31, 2026 23:55
Modules were collected and handed to the classic-script evaluator, which
failed on the first line of every real module entry point:
SyntaxError: expected token '.', got '{' in import.meta at line 1, col 7
Column 7 is the character after `import `, and the character is `{`. In
the classic-script goal the only legal continuation is `import.meta` or
`import(...)`, so the parser read `import {x} from '...'` as a broken
`import.meta`. Nothing was wrong with the pages.
Parsing modules without resolving imports would not have moved anything,
because module entry points import. So this is the loader as well:
- `BlitzModuleLoader` resolves specifiers with `Url::join` and fetches
them over the document's existing `ScriptFetcher`, synchronously, the
way a classic `<script src>` already blocks.
- Boa addresses modules by `Path` and the web by `Url`. The path is used
as an opaque key holding the URL's own string rather than being routed
through path resolution, whose rules differ from the URL spec's.
- A resolved-URL cache, which the spec requires: without it a diamond
import instantiates the shared module twice and each half of the page
gets its own copy of that module's state.
- `import.meta.url`, which asset helpers read.
The loader can only be installed when the context is built, but an
embedder supplies its fetcher afterwards, so the fetcher now lives in a
cell shared between the document and the loader.
`nomodule` is now honoured. It only becomes meaningful once modules run:
a page shipping both bundles would otherwise mount its application twice.
Six tests, including a two-module graph fetched over a real loopback
socket. Every accept loop in them has a deadline, so a regression fails
the suite instead of hanging it.
Two gaps left by module support on its own.
`<script type="importmap">` is how a page that ships unbundled modules
names its dependencies. `import { h } from "preact"` is a bare
specifier: it means nothing without the map, so the graph stopped at the
entry point's first dependency. Implemented is the resolution half of
the spec — normalisation at parse time, longest-prefix matching for keys
ending in `/`, and scopes selected by the importing module's URL.
`integrity` is not, being a fetch-time concern.
Maps are installed across the whole batch before any script in it runs.
A page may write its map after the module that needs it, and a map
installed too late is the same as no map: the module has already failed
to resolve.
A malformed map yields an empty one rather than an error. What the page
then reports is "this specifier does not resolve", naming the specifier,
which is more use than a parse error naming a byte offset. An unmapped
bare specifier now says exactly that instead of 404ing on an invented
URL the page never wrote.
Classic scripts now carry their own URL as well. They can still call
dynamic `import()`, and the specifier resolves against the script, not
the document: a bundle served from `/assets/` calling
`import("./chunk.js")` was looking for the chunk beside the HTML.
`<script type="module">`, `importmap` and classic now come off one
`ScriptKind` rather than a pair of booleans that could both be wrong.
…raph
Three things module support needs to be safe on a real page.
**Top-level `await` is not a failure.** A module that opens with
`await fetch(...)` has an unsettled evaluation promise when its script
returns, and a browser settles it on a later turn of the event loop.
Reporting that at evaluation time would fire an error against every
module on the modern web that waits for anything. Unsettled evaluations
are now kept and checked on each poll, so a rejection that arrives late
is still attributed to its module instead of leaving a half-mounted page
with nothing anywhere saying why.
**JSON modules.** `import config from "./x.json" with { type: "json" }`
builds a synthetic module with one default export. Not a source-text
module: running JSON through the JavaScript parser accepts what JSON does
not and rejects the object literal most JSON files begin with.
**A bound on the graph.** Module fetches are synchronous on the document
thread, so an unbounded graph is an unbounded hang with a blank window
and an empty console — the browser looks broken rather than the page.
4096 modules per document is far past any real application and still
terminates.
A module is a deferred script, and a browser runs every non-deferred classic script before any deferred one. Document order alone was right until modules existed. It is now wrong in a way that bites: an inline classic script writing `window.__CONFIG__` after a module tag runs *before* that module in a browser, and the module reads the config it expects. `run_pending_scripts` now makes two passes over each batch: classic non-deferred scripts in document order, then modules and `defer` scripts together, also in document order. `defer` is honoured only on external classic scripts, as the spec ignores it on inline ones, and `async` takes a script back out of the deferred group. The expected sequence in the test is measured, not transcribed from the spec: the fixture's exact markup was served to a stock engine over loopback and the recorded order is what it produced. A page mixing all five script forms is the only way to see that `defer` and `module` interleave by document position rather than forming two groups. Also widens the fixture deadlines and stops each fixture server once it has served every route. One full-suite run failed on a 5s loopback read that lost to a concurrent compile — a timeout there never means what the test is asking about. The early exit is what keeps the generous deadlines free: the suite spends 0.2s here rather than the 30s it would take to sit out every backstop.
The 0.4.0 release moved every published engine crate but left these two behind.
Their manifests are correct on master — the engine arrives through
`workspace = true`, which is `^0.4.0` — but 0.7.2 is what is on the registry
and 0.7.2 went out carrying `^0.3.0-beta.6`.
A version already published cannot be corrected in place, so the fix is a new
version rather than a re-release.
The effect on a consumer is not a stale renderer, it is two engines. A crate
taking ps-blitz ^0.4 and dioxus-native ^0.7.2 resolves both 0.4.0 and
0.3.0-beta.6, and cargo treats them as unrelated crates that happen to export
the same names:
expected `blitz_script::document::ScriptDocument`, found `ScriptDocument`
with both types pointing at the same file. chuzz hits exactly this moving to
0.4, and cannot land its pin bump until this is on the registry.
Nothing here changes code. Only the version these two crates claim, and the
workspace ranges that name them.
Three consecutive builds of a consuming app shipped a completely opaque window while every test it had stayed green. Those tests read the stylesheet source and the JavaScript that writes the alpha; neither looks at a pixel, so neither could tell a working chain from a broken one, and a screenshot was the only feedback available. `window_alpha_admits_light` renders the declaration that app actually ships and asserts the backdrop comes through at low alpha, is hidden at full alpha, and moves continuously between them. `supports_gated_color_mix` settles a suspicion rather than a bug. Lightning CSS emits `color-mix` as progressive enhancement, keeping an opaque fallback outside `@supports (color: color-mix(in lab, red, red))` and the real value inside it, and in one bundle 205 of 501 uses are wrapped that way. If the query did not match, every one of them would silently render at its fallback. It does match, for gated rules and for gated custom properties, and `color-mix` with `transparent` produces alpha correctly. Worth having as a one second answer, because "the engine is quietly taking fallbacks" is expensive to re-litigate from scratch.
ES module support is new capability in `blitz-script`, and nothing downstream can ask for it until it is released. The patch, not the minor: the change is additive. `<script type="module">` used to be handed to the classic evaluator, so nothing that works today stops working; what changes is that a case which previously threw a SyntaxError now runs. No public type or signature is removed or narrowed. The in-repo ranges stay at `^0.4.0`, which accepts 0.4.1, so the whole bump is the one line it should be. `ps-dioxus-native` and `ps-dioxus-native-dom` move separately to 0.7.3 on their own version line, in the commit below.
This was referenced Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
One PR, eight commits, with the version bump. Supersedes #80, #81 and #82, which I am closing in favour of this.
All three source branches cherry-picked onto master with no conflicts.
ES modules (#81), and why it matters
<script type="module">was handed to the classic evaluator.blitz-script's own comment said so: "modulescripts are treated as classic scripts for now."Measured across a 104-site corpus, that was the largest unfixed rendering defect: 15 sites. It was originally logged as "
import.metadoes not parse", which is wrong and misdirects. The real signature is:Column 7 is immediately after
import, and the character is{— that isimport {x} from '...', a module parsed as a classic script. A second site said it outright:invalid import.meta expression outside a module.A partial fix would not have moved the number: every one of those 15 sites uses
import ... from, so it needed the loader, not just module parsing. This delivers that and more — import maps, JSON modules, dynamic import from classic scripts, and deferred-script ordering.Version: 0.4.1, not 0.5.0
Additive. Nothing that works today stops working; a case that previously threw a SyntaxError now runs. No public type or signature is removed or narrowed.
In-repo ranges stay
^0.4.0, which accepts 0.4.1, so the workspace bump is the one line it should be.ps-dioxus-nativeandps-dioxus-native-dommove separately to 0.7.3 on their own version line, in50b36e6e.Recovered tests (#82)
window_alpha_admits_light.rsandsupports_gated_color_mix.rs— 215 lines written on a local branch on 2026-08-19 that never landed. Additive, no source changes.They are the entire result of sweeping ~40 local-only branches across ps-blitz, agencyzero, tauri-runtime-blitz, ps-observability, ps-boa and chuzz for stranded work. Everything else was already merged in evolved form — including agencyzero's
fix/color-wheel-first-paint, which looked like 40 commits and 114 files until each one turned out to be patch-equivalent to master.On local test state
Running the full workspace locally is red on this branch — and redder on master:
So the stack strictly improves on master and introduces none of it. The residue looks like local environment rather than real breakage: a newer local rustc raising lints CI does not have (
non-binding let on a future), and wasm guest tests needing an artifact frompackages/blitz-wasm/guest, which the workspace excludes. CI reported CLEAN on all three source PRs.I am deliberately letting CI be the judge here rather than claiming green from my own machine. If CI disagrees with that reading, the failures are real and this needs work before merging.