Skip to content

fix(shims/script): emit <link rel="stylesheet"> for next/script stylesheets prop#1646

Draft
james-elicx wants to merge 1 commit into
mainfrom
fix/issue-1517-script-stylesheets-nonce
Draft

fix(shims/script): emit <link rel="stylesheet"> for next/script stylesheets prop#1646
james-elicx wants to merge 1 commit into
mainfrom
fix/issue-1517-script-stylesheets-nonce

Conversation

@james-elicx
Copy link
Copy Markdown
Member

Summary

Fixes part of #1517 — the next/script stylesheets prop is now honoured.

The vinext shim ignored the stylesheets prop entirely. <Script src="/x.js" stylesheets={['/x.css']} /> would (a) emit only the <script>/preload and silently drop the associated stylesheet and (b) leak the prop onto the rendered <script> as a stylesheets="..." attribute for beforeInteractive.

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

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

Scope choice

Issue #1517 lists three sub-items. Per the issue workflow (prefer smaller PRs scoped to one part):

Part Status
1. Stylesheets prop Fixed in this PR.
2. Nonce propagation Already handled — see buildBeforeInteractiveScriptProps, the ReactDOM.preload nonce branch, loadClientScript's el.setAttribute('nonce', …), and the existing "Script nonce resolution" test suite (tests/script.test.ts:325-468, plus #1608/#1607). Spot-checked: SSR <script> tags, the React Float preload link, and the client-imperative <script> all receive the resolved nonce.
3. Bootstrap preinit Out of scope — separate, much larger change tracked under #1328.

Test plan

  • pnpm test tests/script.test.ts — 28 pass (4 new tests added for the stylesheets prop).
  • pnpm test tests/script-head-ordering.test.ts — 8 pass (no regression on inline beforeInteractive ordering).
  • pnpm run check — formatter, lint, and type checks all clean.

…sheets prop

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.
@james-elicx
Copy link
Copy Markdown
Member Author

/bigbonk review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 28, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1646

commit: 606af68

@github-actions
Copy link
Copy Markdown

Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
web preview production

@james-elicx
Copy link
Copy Markdown
Member Author

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(shims/script): emit <link rel="stylesheet"> for next/script stylesheets prop

Verdict: Approve

Clean, well-scoped PR. Verified against the Next.js reference implementation at .nextjs-ref/packages/next/src/client/script.tsx and .nextjs-ref/packages/next/src/client/set-attributes-from-props.ts.

Correctness

All integration points are handled:

  • SSR: preinitStylesheetsForSSR calls ReactDOM.preinit(href, { as: 'style' }) for every strategy, matching Next.js's app-router appDir block (lines 309-313).
  • Client load: insertClientStylesheets mirrors Next.js's insertStylesheets (lines 29-60) — prefers ReactDOM.preinit for Float-based dedup, falls back to direct DOM <link> injection.
  • Attribute filtering: stylesheets is destructured out of rest in both Script and loadClientScript, and added to the RESERVED set in collectBeforeInteractiveAttributes. This matches Next.js's ignoreProps array in set-attributes-from-props.ts:16.
  • useEffect deps: stylesheets is correctly in the dependency array.

Intentional improvement over Next.js

In loadClientScript, insertClientStylesheets is called before the loadedScripts.has(key) early return. In Next.js's loadScript, insertStylesheets runs after the LoadCache early return (line 78-80 vs 151), meaning re-mounted scripts with the same key silently drop their stylesheets. The vinext version is more robust here — good call.

Tests

6 new tests cover the key scenarios: SSR emission, multi-stylesheet lists, attribute non-leakage, omitted/empty arrays, and client-side handleClientScriptLoad. All 28 script tests and all 8 head-ordering tests pass.

Looks good.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 28, 2026

Review posted as APPROVE on PR #1646.

The PR correctly implements the stylesheets prop for the next/script shim, matching Next.js's behavior with one minor intentional improvement (inserting stylesheets before the already-loaded early return for more robust re-mount handling). All tests pass — 28 in script.test.ts and 8 in script-head-ordering.test.ts.

github run

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