Skip to content

fix(lazy): parse-safe retry import, and only cache-bust native ESM - #62

Merged
chiefcll merged 2 commits into
mainfrom
fix/lazy-retry-dependency-url
Sep 11, 2026
Merged

chiefcll merged 2 commits into
mainfrom
fix/lazy-retry-dependency-url

Conversation

@chiefcll

@chiefcll chiefcll commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Two fixes to the retry landed in #61, both found by watching it in production. The first one is a live outage fix — please take that commit even if the second needs discussion.

fix(lazy): build the retry import with Function so old engines can parse — URGENT

#61 wrote the cache-busted retry as a literal import(/* @vite-ignore */ url). @vite-ignore is precisely what stops Vite rewriting it, and in a consumer with no SystemJS transform — plain iife output plus a syntax-only build.target, which Rollup cannot rewrite either — the token survived into the bundle verbatim.

Dynamic import() arrived in Chrome 63. On anything older the script fails to parse, so nothing in it runs.

This shipped. Every Samsung Tizen 4.0 set (Chrome 56) failed to boot, logging SyntaxError: Unexpected token import with no app version or platform attached — because no app code ever executed. Onset was 29 seconds after the release's first session, with zero occurrences in the preceding 30 days.

Platforms running @vitejs/plugin-legacy were unaffected: it rewrites the call to module.import(). That is exactly why webOS, Xumo and Vizio all looked correct when I checked their bundles, and why the one platform without the plugin went unnoticed.

The fix builds the importer at runtime:

nativeImport = new Function('u', 'return import(u)');

Inside a Function body the token is just text until it is compiled, and that compilation is guarded. Old engines throw a SyntaxError; a CSP without unsafe-eval throws an EvalError. Either way it resolves to null and the caller re-runs the loader — the same fallback every SystemJS failure already takes, so the retry is never worse than not cache-busting. Resolved once and cached, null included.

Verified end to end, not by inspection

bundle es-check es2017
deployed bundle (as shipped) failsSyntaxError: Unexpected token (1:835585), the dynamic import
real Tizen app built against this branch passes — "All files are ES8 compatible"

The retry code is present in the passing bundle (chunkRetry appears), so this is the fix working, not the feature being dropped.

fix(lazy): only cache-bust the retry for native ESM failures

SystemJS error #3 reads <failedUrl>, <parentUrl> (SystemJS …). When what failed is a dependency of the requested chunk, the first URL is that dependency. #61 matched the first .js URL anywhere, so for those failures it cache-busted and re-imported the wrong module — and on success lazy would read .default off it, yielding a blank route or somebody else's component.

Observed on webOS, seven first-attempt failures shaped like:

…/TheaterPlayer.nav-legacy-1MzaTqJc.js, …/DiscoverV2Hero.page-legacy-CKnRyt-q.js (SystemJS …)

alongside one correct direct-route retry carrying ?chunkRetry=1 — which is how the retry was confirmed working at all. Both shapes came from the same matcher; only one was right.

Now only native ESM's message is matched. It names exactly one module, always the one handed to import() — verified across a production fleet, where every such failure named a route chunk (Theater.page-*, DiscoverV2Hero.page-*, …) and never a _shared or util chunk, over ~230 impacted sessions. Chrome's and Firefox's wordings are both covered; Safari names no module and falls through safely.

Every SystemJS shape falls through to re-running fn, which is both safe (no URL is guessed) and sufficient (SystemJS drops the failed load from its registry).

Tests — 18, all with negative controls

Each mutation fails exactly the tests describing it and nothing else:

mutation result
revert to the inline @vite-ignore import 2 failed, 16 passed
remove the try/catch around Function 1 failed, 17 passed
matcher reverted to #61's first-URL-anywhere 4 failed, 14 passed
Firefox wording dropped from the matcher 1 failed, 17 passed

Includes a source-text canary — reverting to the inline form fails loudly rather than silently re-shipping the outage — and a stubbed-Function case covering the old-engine / CSP path.

$ pnpm run tsc        → exit 0
$ npx prettier --check → all files use Prettier style
$ npx eslint .         → 0 errors (154 pre-existing warnings elsewhere in src/)
$ npx vitest run       → 19 files, 204 tests passed

Worth knowing for whoever releases this

The consuming app had no way to catch this: its check-browser-compat script skips any file matching -legacy, and the Tizen build's only output is index-legacy.js — so that platform is scanned by nothing, and it ships no polyfill layer either. That is the consumer's gap to close, not this library's, but it is why a parse-level break reached devices.

🤖 Generated with Claude Code

chiefcll and others added 2 commits September 11, 2026 15:24
Found by watching the retry in production. SystemJS error #3 reads
`<failedUrl>, <parentUrl> (SystemJS …)`, and when what failed is a
*dependency* of the requested chunk the FIRST url is that dependency, not
the module we asked for. Matching the first URL anywhere in the message
therefore cache-busted and re-imported the wrong module — and on success
`lazy` would read `.default` off it, yielding a blank route or somebody
else's component.

Observed on webOS: seven first-attempt failures shaped
`…/TheaterPlayer.nav-legacy-*.js, …/DiscoverV2Hero.page-legacy-*.js
(SystemJS …)`, alongside one correct direct-route retry that did carry
`?chunkRetry=1`.

Match only native ESM's message, which names exactly one module and always
the one handed to `import()` — confirmed across a production fleet, where
every such failure named a route chunk and never one of its dependencies.
Chrome's and Firefox's wordings are both covered.

Every SystemJS shape now falls through to re-running `fn`, which is both
safe and sufficient there: SystemJS drops the failed load from its
registry, so a plain re-run genuinely re-fetches. The cache-buster exists
solely because native ESM memoises the *failure* in its module map, so
restricting it to that case loses nothing — including for the direct
SystemJS failure that was previously being busted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cache-busted retry was written as a literal
`import(/* @vite-ignore */ url)`. `@vite-ignore` is precisely what stops
Vite rewriting it, so in a consumer that has no SystemJS transform — plain
`iife` output with a syntax-only `build.target`, which Rollup cannot
rewrite either — the token survived into the bundle verbatim.

Dynamic `import()` arrived in Chrome 63. On anything older the script fails
to PARSE, so nothing in it runs. This shipped: every Samsung Tizen 4.0 set
(Chrome 56) failed to boot, logging `SyntaxError: Unexpected token import`
with no app context at all because no app code ever executed. Platforms
running `@vitejs/plugin-legacy` were unaffected — it rewrites the call to
`module.import()` — which is why webOS, Xumo and Vizio looked fine and the
gap went unnoticed.

Build the importer with `new Function('u', 'return import(u)')` instead.
Inside a Function body the token is just text until it is compiled, and
that compilation is guarded: old engines throw a SyntaxError and a CSP
without `unsafe-eval` throws an EvalError, so both fall back to re-running
the loader — the same fallback every SystemJS failure already takes.
Resolved once and cached, including the null.

Verified end to end rather than by inspection. Building a real Tizen app
against this and running `es-check es2017` over the bundle passes, while
the same check on the deployed bundle fails at the dynamic import. Tests
add a canary on the source text, so reverting to the inline form fails
loudly, plus a stubbed-Function case covering the CSP/old-engine path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chiefcll chiefcll changed the title fix(lazy): only cache-bust the retry for native ESM failures fix(lazy): parse-safe retry import, and only cache-bust native ESM Sep 11, 2026
@chiefcll
chiefcll merged commit 8025824 into main Sep 11, 2026
1 check passed
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