feat!: migrate Nuxt Scripts v2 to Unhead 3#832
Conversation
Nuxt 4.5 adopts the Unhead 3 script APIs. Move consumers to scoped instances, use abortable readiness resolvers for provider SDKs, and replace local trigger implementations with Unhead factories. BREAKING CHANGE: Nuxt Scripts now requires Nuxt 4.5+, @unhead/vue 3.2+, and unhead 3.2+.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
commit: |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (36)
📝 WalkthroughWalkthroughThe update moves script readiness and ownership to lifecycle-aware Unhead APIs. It adds abortable operations, per-consumer disposal, shared script facades, trigger cleanup, vendor readiness resolvers, and component teardown. Devtools and server handlers gain bounded request processing, cancellation, cache scoping, and streamed responses. Nuxt and Unhead compatibility requirements are tightened, with corresponding documentation, dependency updates, and lifecycle tests. Estimated code review effort: 5 (Critical) | ~120 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/script/src/devtools.ts (1)
100-135: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winDecode the full body once instead of per chunk to avoid corrupting multi-byte UTF-8.
body += chunk.toString()decodes eachBufferindependently. If a multi-byte UTF-8 sequence straddles a chunk boundary (realistic for payloads above ~64KiB, and the cap here is 2MiB), eachtoString()emits replacement characters andJSON.parsefails with a spurious 400. Accumulate the bytes and decode once, mirroring theUint8Array+TextDecoderapproach already used inpackages/script/src/runtime/server/proxy-handler.ts.🐛 Proposed fix
if (req.method === 'POST') { - let body = '' + const chunks: Buffer[] = [] let size = 0 let finished = false @@ function onAborted() { finished = true - body = '' + chunks.length = 0 cleanup() } function onData(chunk: Buffer) { if (finished) return size += chunk.byteLength if (size > DEVTOOLS_API_MAX_BODY_SIZE) { finished = true - body = '' + chunks.length = 0 cleanup() @@ res.end('payload too large') return } - body += chunk.toString() + chunks.push(chunk) } function onEnd() { if (finished) return finished = true cleanup() try { - const data = JSON.parse(body) + const data = JSON.parse(Buffer.concat(chunks).toString('utf8')) scriptsState = { ...data, updatedAt: Date.now() } res.statusCode = 200 res.end('ok') } catch { res.statusCode = 400 res.end('invalid json') } - body = '' + chunks.length = 0 }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/script/src/devtools.ts` around lines 100 - 135, Update the request-body accumulation in the onData and onEnd handlers to retain raw Buffer/Uint8Array chunks and decode the complete body only once after receiving the end event, following the existing TextDecoder approach in proxy-handler.ts. Preserve the size-limit handling and JSON parsing behavior while eliminating per-chunk UTF-8 decoding.
🧹 Nitpick comments (1)
packages/script/src/runtime/registry/google-maps.ts (1)
54-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffExtract the shared "install-and-restore global readiness handler" pattern. All three resolvers reimplement the same
previousReadycapture →restoreReady→onReady(invoke prior handler with dev-error logging → resolve/reject) sequence. A single helper (parameterized by getter/setter/deleter for the global slot and an optional "verify API present else reject" callback) would remove the duplication and keep the restore semantics consistent.
packages/script/src/runtime/registry/google-maps.ts#L54-L77: replace the inlinemaps.__ib__install/restore block with the shared helper (resolve{ maps }, no reject path).packages/script/src/runtime/registry/crisp.ts#L56-L82: use the helper forwindow.CRISP_READY_TRIGGER, passing the$crisp?.ispresence check for the reject path.packages/script/src/runtime/registry/youtube-player.ts#L50-L76: use the helper forwindow.onYouTubeIframeAPIReady, passing thewindow.YTpresence check for the reject path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/script/src/runtime/registry/google-maps.ts` around lines 54 - 77, Extract the shared global readiness-handler installation and restoration logic into one helper, including prior-handler invocation, dev-error logging, and restoration semantics. In packages/script/src/runtime/registry/google-maps.ts lines 54-77, replace the inline block while resolving { maps } without a reject callback; apply the helper in packages/script/src/runtime/registry/crisp.ts lines 56-82 with the $crisp?.is presence check for rejection; and apply it in packages/script/src/runtime/registry/youtube-player.ts lines 50-76 with the window.YT presence check for rejection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/script/src/module.ts`:
- Around line 537-552: Update the Unhead package resolution around
readPackageJSON to iterate through nuxt.options.modulesDir and call it
separately for each modules directory, selecting the first valid package result
for `@unhead/vue` and unhead. Preserve the existing incompatibleUnheadPackages
validation and error behavior after resolution.
---
Outside diff comments:
In `@packages/script/src/devtools.ts`:
- Around line 100-135: Update the request-body accumulation in the onData and
onEnd handlers to retain raw Buffer/Uint8Array chunks and decode the complete
body only once after receiving the end event, following the existing TextDecoder
approach in proxy-handler.ts. Preserve the size-limit handling and JSON parsing
behavior while eliminating per-chunk UTF-8 decoding.
---
Nitpick comments:
In `@packages/script/src/runtime/registry/google-maps.ts`:
- Around line 54-77: Extract the shared global readiness-handler installation
and restoration logic into one helper, including prior-handler invocation,
dev-error logging, and restoration semantics. In
packages/script/src/runtime/registry/google-maps.ts lines 54-77, replace the
inline block while resolving { maps } without a reject callback; apply the
helper in packages/script/src/runtime/registry/crisp.ts lines 56-82 with the
$crisp?.is presence check for rejection; and apply it in
packages/script/src/runtime/registry/youtube-player.ts lines 50-76 with the
window.YT presence check for rejection.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 12b3d139-67b6-42fa-985b-ee8e9481e712
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (56)
docs/content/docs/1.getting-started/2.installation.mddocs/content/docs/3.api/1.use-script.mddocs/content/docs/4.migration-guide/2.v1-to-v2.mdpackage.jsonpackages/devtools-app/composables/rpc.tspackages/devtools-app/composables/state.tspackages/devtools-app/utils/fetch.tspackages/script/package.jsonpackages/script/src/devtools.tspackages/script/src/module.tspackages/script/src/registry-types.jsonpackages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vuepackages/script/src/runtime/components/GoogleMaps/useGoogleMapsResource.tspackages/script/src/runtime/components/ScriptCarbonAds.vuepackages/script/src/runtime/components/ScriptCrisp.vuepackages/script/src/runtime/components/ScriptIntercom.vuepackages/script/src/runtime/components/ScriptLemonSqueezy.vuepackages/script/src/runtime/components/ScriptPayPalButtons.vuepackages/script/src/runtime/components/ScriptPayPalMessages.vuepackages/script/src/runtime/components/ScriptStripePricingTable.vuepackages/script/src/runtime/components/ScriptVimeoPlayer.vuepackages/script/src/runtime/composables/useScript.tspackages/script/src/runtime/composables/useScriptEventPage.tspackages/script/src/runtime/composables/useScriptTriggerIdleTimeout.tspackages/script/src/runtime/composables/useScriptTriggerInteraction.tspackages/script/src/runtime/composables/useScriptTriggerServiceWorker.tspackages/script/src/runtime/devtools-standalone-bridge.client.tspackages/script/src/runtime/npm-script-stub.tspackages/script/src/runtime/registry/crisp.tspackages/script/src/runtime/registry/google-maps.tspackages/script/src/runtime/registry/speedcurve.tspackages/script/src/runtime/registry/usercentrics.tspackages/script/src/runtime/registry/youtube-player.tspackages/script/src/runtime/server/instagram-embed.tspackages/script/src/runtime/server/proxy-handler.tspackages/script/src/runtime/server/utils/cache-config.tspackages/script/src/runtime/server/utils/cached-upstream.tspackages/script/src/runtime/types.tspackages/script/src/runtime/utils/abortable-promise.tspackages/script/src/runtime/utils/after-next-paint.tspackages/script/src/runtime/utils/nuxt-ready-script-trigger.tspnpm-workspace.yamltest/e2e/base.test.tstest/e2e/basic.test.tstest/fixtures/unhead-v3/package.jsontest/types/types.test-d.tstest/unit/abortable-promise.test.tstest/unit/cached-upstream-lifecycle.test.tstest/unit/google-maps-lifecycle.test.tstest/unit/npm-script-stub-lifecycle.test.tstest/unit/registry-readiness.test.tstest/unit/script-trigger-lifecycle.test.tstest/unit/speedcurve-after-next-paint.test.tstest/unit/use-script-lifecycle.test.tstest/unit/youtube-player-lifecycle.test.tsvitest.config.ts
# Conflicts: # docs/content/docs/1.getting-started/2.installation.md # docs/content/docs/3.api/1.use-script.md
🔗 Linked issue
Supersedes #829
Related to unjs/unhead#840
❓ Type of change
📚 Description
Nuxt 4.5 provides the Unhead 3 script lifecycle APIs needed by #829. This folds that cleanup work into the v2 migration: dependencies require Nuxt 4.5 and Unhead 3.2,
useScriptuses consumer scopes, provider SDKs useresolve({ waitFor }), and trigger helpers use Unhead factories.Cleanup remains app-owned for
$scripts, devtools observers, reloads, and provider callbacks. Component callers can release their own scope withdispose().The live-provider fixture now loads one real SDK per page. It also covers Clarity's current bootstrap hosts so its child SDK, collection requests, and consent sync stay on the first-party proxy.
@unhead/vue3.2+, andunhead3.2+.useScript()exposes the Unhead consumer scope, includingsignal,script, anddispose().UseScriptTriggerfunctions.📝 Migration
Run
npx nuxi@latest upgrade --force, then update any direct Unhead dependencies to 3.2 or newer. Usedispose()to release one consumer scope;remove()still unloads the shared script for every consumer.See
docs/content/docs/4.migration-guide/2.v1-to-v2.mdfor the full guide.✅ Verification
pnpm lintpnpm typecheckpnpm buildpnpm test:run: 978 passed, 8 skipped, 3 todo, no type errorspnpm test:e2e-dev: 84 passed against live third-party SDKs