fix(lazy): parse-safe retry import, and only cache-bust native ESM - #62
Merged
Merged
Conversation
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>
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.
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-ignoreis precisely what stops Vite rewriting it, and in a consumer with no SystemJS transform — plainiifeoutput plus a syntax-onlybuild.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 importwith 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-legacywere unaffected: it rewrites the call tomodule.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:
Inside a
Functionbody the token is just text until it is compiled, and that compilation is guarded. Old engines throw aSyntaxError; a CSP withoutunsafe-evalthrows anEvalError. Either way it resolves tonulland 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,nullincluded.Verified end to end, not by inspection
es-check es2017SyntaxError: Unexpected token (1:835585), the dynamic importThe retry code is present in the passing bundle (
chunkRetryappears), so this is the fix working, not the feature being dropped.fix(lazy): only cache-bust the retry for native ESM failuresSystemJS 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.jsURL anywhere, so for those failures it cache-busted and re-imported the wrong module — and on successlazywould read.defaultoff it, yielding a blank route or somebody else's component.Observed on webOS, seven first-attempt failures shaped like:
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_sharedor 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:
@vite-ignoreimporttry/catcharoundFunctionIncludes a source-text canary — reverting to the inline form fails loudly rather than silently re-shipping the outage — and a stubbed-
Functioncase covering the old-engine / CSP path.Worth knowing for whoever releases this
The consuming app had no way to catch this: its
check-browser-compatscript skips any file matching-legacy, and the Tizen build's only output isindex-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