Commit Graph

11 Commits

Author SHA1 Message Date
Nathan Nguyen 7bcbe94a25 fix(script): reject event attributes in hoisted HTML (#2763)
App Router hoisting manually serializes beforeInteractive Script props, bypassing React DOM filtering for string-valued event handlers. Request-influenced on* props could therefore become executable inline attributes in the server response.\n\nReject event-handler names case-insensitively at the raw HTML emission boundary while preserving legitimate attributes such as data-onload. The regression test exercises the actual Script capture and hoisted render path.

Co-authored-by: James <james@eli.cx>
2026-08-03 14:26:42 +01:00
Xplod13 33be48648d fix(script): register src beforeInteractive scripts and mark hoisted output (#2019)
## Summary
- Register `beforeInteractive` Scripts with `src` through
  BeforeInteractiveContext so they are hoisted into <head> ahead of
  interactive, exactly like the inline form (previously they bypassed the
  registry and rendered in source order, after React Float's head content).
- Suppress the client React render for both inline AND src beforeInteractive
  Scripts in the App Router so the hoisted tag is never duplicated.
- Tag every hoisted script with Next.js's `data-nscript="beforeInteractive"`
  marker.
- Map `crossOrigin` -> `crossorigin` and `referrerPolicy` -> `referrerpolicy`
  on hoisted scripts (parity with Next.js set-attributes-from-props).
- Extract `renderBeforeInteractiveInlineScripts` into a dedicated, pure module
  (before-interactive-head.ts) so the emit path is unit-testable.

## Root Cause
The beforeInteractive SSR branch computed `inlineContent = src ? null : ...`
and only registered when `inlineContent !== null`, so any Script with a `src`
was never captured by the registry and never spliced into <head>; it stayed
where the user wrote it, behind React Float's hoisted resource hints, defeating
the before-interactive guarantee. Hoisting src scripts then requires the client
to stop rendering them through React (the React-created <script> never executes
and would only add a duplicate DOM node), so the client dedupe condition is
aligned with the SSR registration condition. The hoisted output also lacked
Next.js's `data-nscript` marker, and REACT_TO_HTML_ATTR omitted
crossOrigin/referrerPolicy, so those attribute names did not match Next.js's
lowercased output.

## References
- Fixes #2016
- Next.js: .nextjs-ref/packages/next/src/client/script.tsx (data-nscript,
  src/inline registry parity), set-attributes-from-props.ts (lowercased attrs)
- Ported: test/e2e/app-dir/script-before-interactive/script-before-interactive.test.ts

## Verification
- CI=true pnpm test tests/script.test.ts tests/script-head-ordering.test.ts
  tests/nextjs-compat/script-nonce.test.ts tests/shims.test.ts -> 1139 passed
- CI=true npx vp check on all changed files -> pass

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:38:06 +01:00
James Anderson 1d26b2b229 fix(shims/script): emit <link rel="stylesheet"> for next/script stylesheets prop (#1646)
Refs #1517.

The vinext `next/script` shim ignored the `stylesheets` prop, so an
`<Script src="/x.js" stylesheets={['/x.css']} />` element emitted only the
`<script>`/preload and silently dropped the associated stylesheet (and
worse, leaked the prop onto the rendered `<script>` as a `stylesheets="..."`
attribute on the beforeInteractive branch).

Mirror the App-Router branch of Next.js's component
(`.nextjs-ref/packages/next/src/client/script.tsx:309-313` and
`:48-59`):

- SSR: call `ReactDOM.preinit(href, { as: 'style' })` for each entry —
  React Float hoists `<link rel="stylesheet">` into `<head>`.
- Client load path (`loadClientScript` + `handleClientScriptLoad`): same
  preinit preferred; fall back to direct `document.head.appendChild` of
  `<link rel="stylesheet">` when `ReactDOM.preinit` is unavailable.
- Destructure `stylesheets` out of `rest` everywhere so it never lands
  as an attribute on the emitted `<script>`.
- Add `stylesheets` to the `RESERVED` set used by the hoisted-script
  attribute collector for inline beforeInteractive scripts.

Scope: stylesheets prop only. Nonce propagation (the other half of #1517)
is already handled across SSR (`buildBeforeInteractiveScriptProps`,
`ReactDOM.preload`) and client (`loadClientScript`) paths — see the
existing "Script nonce resolution" suite. Bootstrap-script preinit is a
separate, larger change tracked under #1328.

Tests: extend `tests/script.test.ts` with a `Script stylesheets prop`
suite covering SSR emission, multi-entry lists, prop leakage on the
rendered `<script>`, omitted/empty lists, and the imperative
`handleClientScriptLoad` path.
2026-05-28 21:18:17 +01:00
James Anderson b463b8eb30 fix(script): translate React DOM prop names to HTML attrs in hoisted beforeInteractive scripts (#1629)
Inline `<Script strategy="beforeInteractive">` content is captured via
BeforeInteractiveContext and spliced directly into <head> as raw HTML by
the SSR pipeline. The attribute collector preserved React's camelCase prop
names verbatim, so `className="foo"` round-tripped as `classname="foo"`
(HTML attribute names are case-insensitive), making CSS selectors on `.foo`
fail to match. Same hazard for `htmlFor`, `httpEquiv`, `acceptCharset`.

Translate those four prop names to their HTML attribute equivalents at the
collection boundary, mirroring Next.js's set-attributes-from-props.ts.

Fixes #1518
2026-05-28 14:03:18 +01:00
Jim Geurts 2af6298a11 fix(shims/script): guard HTMLElement and prefer contextual nonce in SSR (#1608)
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
2026-05-27 09:42:41 +01:00
James Anderson 558cec0328 fix(shims/script): preload <script> sources during SSR via ReactDOM.preload (#1309)
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).
2026-05-18 15:29:07 +01:00
Nathan Nguyen e8cf0dad13 fix(script): honor async false for client scripts (#1286)
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.
2026-05-18 10:43:32 +01:00
Stephen Zhou 14ff303cbd feat: csp support (#795)
* fix: support csp

* tweaks

* tweaks

* tweaks

* tweaks

* tweaks

* tweaks

* clean

* tweaks

* clean

* fix test

* tweaks

* fix lock

* fix test

* trigger ci

* tweaks

* tweaks
2026-04-09 09:14:13 +01:00
Stephen Zhou c17d6941be chore: migrate to vite plus (#535)
* 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
2026-03-15 10:50:13 +00:00
James Anderson 764a496ce7 add oxfmt formatter (#380)
* 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
2026-03-09 14:56:14 +00:00
Steve Faulkner 12fea722b6 Initial public release of vinext 2026-02-24 09:29:39 -06:00