Skip to content

Release 0.4.1: ES module support, dioxus-native 0.7.3, recovered tests - #83

Merged
pathscale merged 8 commits into
masterfrom
release/0.4.1
Sep 1, 2026
Merged

Release 0.4.1: ES module support, dioxus-native 0.7.3, recovered tests#83
pathscale merged 8 commits into
masterfrom
release/0.4.1

Conversation

@pathscale

Copy link
Copy Markdown
Owner

One PR, eight commits, with the version bump. Supersedes #80, #81 and #82, which I am closing in favour of this.

f768c01d release: bump ps-blitz workspace to 0.4.1
8eb270ca test: prove a translucent surface admits what is behind it
50b36e6e release: publish dioxus-native at 0.7.3, against the 0.4 engine
502fb6bc fix(blitz-script): run deferred scripts after the parser-blocking ones
37ca302e docs(blitz-script): state what module support covers and what it does not
d2301d47 feat(blitz-script): JSON modules, deferred settling, a bound on the graph
70a43122 feat(blitz-script): import maps, and dynamic import from classic scripts
0892359a feat(blitz-script): run `<script type="module">` as a module

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: "module scripts 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.meta does not parse", which is wrong and misdirects. The real signature is:

SyntaxError: expected token '.', got '{' in import.meta at line 1, col 7

Column 7 is immediately after import , and the character is { — that is import {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-native and ps-dioxus-native-dom move separately to 0.7.3 on their own version line, in 50b36e6e.

Recovered tests (#82)

window_alpha_admits_light.rs and supports_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:

clippy errors test failures
master 7 5
this branch 2 3

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 from packages/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.

meh 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.
@pathscale
pathscale merged commit 84ef2ed into master Sep 1, 2026
11 checks passed
@pathscale
pathscale deleted the release/0.4.1 branch September 1, 2026 00:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant