Some SSR/edge runtimes polyfill `document` but not the `HTMLElement`
constructor. The previous nonce resolver hit `instanceof HTMLElement`
unguarded and crashed the render with
"ReferenceError: HTMLElement is not defined".
The resolver also fell through to DOM auto-detection whenever `window`
was defined, even when a contextual server nonce was available — DOM
auto-detection is a browser convenience and shouldn't be reached on the
server.
This change:
- Guards `HTMLElement` before `instanceof`, falling back to
`getAttribute("nonce")` when the constructor is unavailable.
- Prefers explicit nonce, then contextual server nonce, then DOM
fallback (only when `window` is defined).
- Adds regression coverage for the crash and the resolver ordering,
including the exact minimal repro shape from the upstream report
(window + document defined, HTMLElement absent).
Refs cloudflare/vinext#1607
Mirror Next.js's App Router behavior at
.nextjs-ref/packages/next/src/client/script.tsx:298-376: when SSR-rendering
`<Script src={...} />` with `strategy="afterInteractive"` (the default)
or `strategy="beforeInteractive"`, call `ReactDOM.preload(src, { as: 'script', ... })`
so React Float hoists `<link rel="preload" as="script" href={src} />` into
`<head>`. The script is then fetched in parallel with the streamed HTML.
Before this change, the SSR branch of the Script shim only emitted a
`<script>` tag for `beforeInteractive` and returned `null` for every other
strategy, with no preload. The matching Next.js fixture test
test/e2e/app-dir/app-esm-js/index.test.ts ("should be able to render
nextjs api in app router") asserts:
expect($('head link[href="/test-ext.js"]').length).toBe(1)
expect($('head link[href="/test.js"]').length).toBe(1)
…which fails today because no preload links are emitted.
The existing Image shim already uses this exact React Float pattern
(packages/vinext/src/shims/image.tsx:285-286), so the SSR pipeline
already supports Float hoisting.
Tests:
- Updated existing Script SSR tests in tests/script.test.ts and
tests/shims.test.ts that asserted afterInteractive/default rendered
empty — they now assert the preload <link> is emitted (no <script>
tag), matching Next.js.
- Added tests/nextjs-compat/script-preload.test.ts that ports the
app-esm-js fixture's preload assertions against the app-basic
/script-nonce page (which already exercises afterInteractive and
beforeInteractive strategies via the existing fixture).
- crossOrigin handling matches React's type contract
("anonymous" | "use-credentials" | undefined). React normalises
"anonymous" to `crossorigin=""` in HTML; tests accept both forms.
All shim / script / page / document / nonce tests pass (1151 tests).
Client-side next/script insertion left dynamically created script elements in the browser's forced async state when callers passed async={false}. That breaks ordered third-party script loading because the shim skipped false boolean props instead of clearing the DOM script async flag.
Set script boolean attributes through their DOM properties and explicitly set/remove disabled boolean attributes to clear force async. Cover the regression with a focused handleClientScriptLoad test.
* chore: migrate to vite plus
* Disable typeAware and typeCheck
* Update CI
* Fix CI
* Fix test
* Clean
* Run test with vp
* Try revert
* react: false In test
* Fix test
* Revert "Try revert"
This reverts commit 009da10473.
* Update
* Update
* Try revert ci changes
* revert
* Run vp migrate
* Disable typeAware and typeCheck for now
* Better resolve for test
* Use vp dev instead of vite
* Update expect
* Fix NormalizeManifestModuleId
* Try increase timeout
* Update to use vp
* Try new check
* Bring back npx vp
* Migrate CI
* Make next-intl resolvable
* Update
* Update
* Update
* add oxfmt formatter: config, scripts, CI, editor setup, docs
* rebuild lockfile
* fix: add Format to required checks list, remove dead ignore pattern
* run fmt
* add format to agents.md again