Skip to content

Commit 0342a4e

Browse files
authored
feat!: migrate Nuxt Scripts v2 to Unhead 3.3.1 (#832)
1 parent b8f3533 commit 0342a4e

83 files changed

Lines changed: 1706 additions & 2706 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/content/docs/1.getting-started/2.installation.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ description: Install Nuxt Scripts in an existing Nuxt project.
55

66
## Quick Start
77

8-
Nuxt Scripts 1.x requires Nuxt 3.16 or newer.
8+
Nuxt Scripts 2 requires Nuxt 4.5.1 or newer and Unhead 3.3.1 or newer. Upgrade an
9+
existing project before installing:
10+
11+
```bash
12+
npx nuxi@latest upgrade --force
13+
```
914

1015
Run:
1116

‎docs/content/docs/1.guides/1.script-triggers.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export default defineNuxtConfig({
9696

9797
### User Interaction
9898

99-
[`useScriptTriggerInteraction()`{lang="ts"}](/docs/api/use-script-trigger-interaction){lang="ts"} resolves on the first configured interaction:
99+
[`useScriptTriggerInteraction()`{lang="ts"}](/docs/api/use-script-trigger-interaction){lang="ts"} loads on the first configured interaction:
100100

101101
::code-group
102102

‎docs/content/docs/3.api/1.use-script.md‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ Unhead's [complete script example](https://unhead.unjs.io/docs/head/guides/core-
5050

5151
NuxtScriptsextendsUnhead's [script triggers and warmup options](https://unhead.unjs.io/docs/head/guides/core-concepts/loading-scripts/#how-do-i-control-when-scripts-load) with these options:
5252

53-
-`use`-Thefunction to resolve the script.
53+
-`resolve`-ResolvetheloadedSDKwithlifecycle-bound`signal`and`waitFor`helpers.
54+
-`use`-LegacysynchronousorasyncSDKresolver. Prefer`resolve`forcallback-basedreadiness.
5455
-`trigger`- [TriggeringScriptLoading](/docs/guides/script-triggers)
5556
-`bundle`-Control [first-partybundling](/docs/guides/first-party).
5657
-`proxy`-Enableordisablesupportedcollectionproxyingforaregistryscript.
@@ -119,10 +120,34 @@ Outside the Partytown path, the returned object includes:
119120
-`proxy`-Atypedproxythatqueuescallsuntilloadingfinishes
120121
-`status`-Reactiverefwiththescriptstatus: `'awaitingLoad'`|`'loading'`|`'loaded'`|`'error'`|`'removed'`
121122
-`load()`{lang="ts"} -Functiontomanuallyloadthescript
122-
-`remove()`{lang="ts"} -FunctiontoremovethescriptfromtheDOM
123+
-`signal`-An`AbortSignal`scopedtothiscomposableconsumer
124+
-`dispose()`{lang="ts"} -Releasethisconsumer's callbacks and triggers without removing the shared script
125+
-`script`-ThesharedUnheadscriptinstance
126+
-`remove()`{lang="ts"} -Removethesharedscriptforeveryconsumer
123127
-`reload()`{lang="ts"} -Functiontoremoveandreloadthescript (seebelow)
124128
-`onLoaded()`{lang="ts"} and`onError()`{lang="ts"} -Scriptlifecyclecallbacks
125129

130+
Eachcallreceivesitsownconsumerscope. Vuedisposesthatscopewhenits
131+
componentunmounts, whilethesharedscriptremainsavailabletoothercallers.
132+
Call`remove()`{lang="ts"} onlytoremovethescriptglobally.
133+
134+
### Lifecycle-awareSDKreadiness
135+
136+
Use`resolve({ waitFor })`{lang="ts"} whenavendorexposesacallbackthatfires
137+
afterthescriptelement's `load` event. Listener cleanup and abort rejection are
138+
thentiedtothesharedscriptlifecycle.
139+
140+
```ts
141+
const sdk = useScript<{ ready: true }>('https://example.com/sdk.js', {
142+
resolve: ({ waitFor }) => waitFor<{ ready: true }>((resolve) => {
143+
window.onExampleReady = () => resolve({ ready: true })
144+
return () => delete window.onExampleReady
145+
}),
146+
})
147+
148+
const api = await sdk.load()
149+
```
150+
126151
### `reload()`{lang="ts"}
127152

128153
Removesthescript, insertsitagain, andre-executesit. UsethisforascriptthatscanstheDOMonceandmustscanagainafterSPAnavigation.

‎docs/content/docs/3.api/3.use-script-trigger-idle-timeout.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The trigger uses a timer after `onNuxtReady`; it does not wait for the browser's
1515
## Signature
1616

1717
```ts
18-
function useScriptTriggerIdleTimeout(options:IdleTimeoutScriptTriggerOptions):Promise<boolean>
18+
function useScriptTriggerIdleTimeout(options:IdleTimeoutScriptTriggerOptions):UseScriptTrigger
1919
```
2020

2121
## Arguments
@@ -31,7 +31,10 @@ export interface IdleTimeoutScriptTriggerOptions {
3131

3232
## Returns
3333

34-
Apromisethatresolvesto`true`whenthetimeoutcompletes. IftheowningVuescopeisdisposedafterthetimerstarts, itstopsthetimerandresolvesto`false`. Ontheserver, andifthescopedisappearsbefore`onNuxtReady`runs, thepromiseremainspending.
34+
AnUnheadtriggerfunction for `scriptOptions.trigger`. It starts the timer after
35+
Nuxt is ready and loads the script when the timeout completes. Disposing the
36+
consumer scope cancels the pending timer, including when disposal happens before
37+
Nuxt becomes ready. The trigger does not install a timer during SSR.
3538

3639
## NuxtConfigUsage
3740

‎docs/content/docs/3.api/3.use-script-trigger-interaction.md‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ links:
1010

1111
Load a script when any configured interaction event occurs.
1212

13-
Listeners are attached inside `onNuxtReady`, so interactions that happen before Nuxt is ready do not count. Passing an empty `events` array leaves the promise pending.
14-
15-
The scope-disposal hook is also registered inside `onNuxtReady`. If the calling scope unmounts before Nuxt becomes ready, the callback can still attach listeners afterward; keep this trigger in a long-lived scope until that cleanup ordering is fixed.
13+
Listeners are attached inside `onNuxtReady`, so interactions that happen before
14+
Nuxt is ready do not count. Passing an empty `events` array throws an error.
1615

1716
## Signature
1817

1918
```ts
20-
function useScriptTriggerInteraction(options:InteractionScriptTriggerOptions):Promise<boolean>
19+
function useScriptTriggerInteraction(options:InteractionScriptTriggerOptions):UseScriptTrigger
2120
```
2221

2322
## Arguments
@@ -38,7 +37,10 @@ export interface InteractionScriptTriggerOptions {
3837

3938
## Returns
4039

41-
Apromisethatresolvesto`true`afterthefirstmatchingevent. Itresolvesto`false`when`target`isnullortheowningscopeisdisposedafterthelistenershavebeenattached. Ontheserver, andwhenthescopedisappearsbefore`onNuxtReady`registerscleanup, itremainspending.
40+
AnUnheadtriggerfunction for `scriptOptions.trigger`. It loads the script after
41+
the first matching event, then removes every listener. Disposing the consumer
42+
scope removes pending listeners, including when disposal happens before Nuxt
43+
becomes ready. A `null` target leaves the script unloaded.
4244

4345
## NuxtConfigUsage
4446

@@ -125,4 +127,4 @@ The `target` option accepts an `EventTarget` that already exists when the compos
125127
-Listenforinteractionsthatindicateuserswillneedthescriptsoon.
126128
-Includekeyboardandtoucheventswhenthefeaturesupportsthoseinputmethods.
127129
-Use`target`tolimitlistenerstotherelevantpartofthepage.
128-
-Ifthescriptmustloadbyadeadlineevenwithoutinteraction, racethistriggeragainstatimeoutinacustompromise.
130+
-Ifthescriptmustloadbyadeadlineevenwithoutinteraction, combinetheeventlistenersandtimeoutinonecustomtriggerfunction.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
title: v1 to v2
3+
description: Migration guide for upgrading from Nuxt Scripts v1.x to v2.0.
4+
---
5+
6+
Nuxt Scripts 2 moves script ownership and SDK readiness onto the lifecycle APIs
7+
introduced in Unhead 3.3.1. This removes component callbacks and trigger listeners
8+
as soon as their consumer unmounts, without tearing down a script still used by
9+
other components.
10+
11+
## Requirements
12+
13+
| Dependency | Required version |
14+
|---|---|
15+
| Nuxt |`>=4.5.1`|
16+
|`@unhead/vue`|`>=3.3.1 <4`|
17+
|`unhead`|`>=3.3.1 <4`|
18+
19+
Upgrade Nuxt and refresh its locked dependencies before installing v2:
20+
21+
```bash
22+
npx nuxi@latest upgrade --force
23+
```
24+
25+
The module now stops setup with an actionable error when either Unhead package
26+
is missing or outside the supported range.
27+
28+
## Consumer scopes
29+
30+
Every `useScript()`{lang="ts"} call now returns an Unhead consumer scope.
31+
Component unmount automatically releases callbacks and trigger listeners owned
32+
by that call.
33+
34+
-`dispose()`{lang="ts"} releases only the current consumer.
35+
-`signal` aborts when you dispose that consumer or any caller removes the shared script.
36+
-`script` points to the shared script instance.
37+
-`remove()`{lang="ts"} still removes the shared script for all consumers.
38+
39+
If application code used `remove()`{lang="ts"} as component cleanup, switch it
40+
to `dispose()`{lang="ts"}. In Vue components, manual cleanup is normally no
41+
longer necessary.
42+
43+
## Custom readiness callbacks
44+
45+
The legacy `use` option remains supported. Callback-driven SDKs should migrate
46+
to `resolve({ waitFor })`{lang="ts"}, which automatically removes listeners and
47+
rejects pending readiness when the script lifecycle ends.
48+
49+
```diff
50+
const script = useScript('https://example.com/sdk.js', {
51+
- use: () => readyPromise.then(() => window.example),
52+
+ resolve: ({ waitFor }) => waitFor((resolve) => {
53+
+ window.onExampleReady = () => resolve(window.example)
54+
+ return () => delete window.onExampleReady
55+
+ }),
56+
})
57+
```
58+
59+
The bundled Google Maps, YouTube Player, Crisp, and Usercentrics integrations
60+
now use this API. `load()`{lang="ts"} resolves only after each vendor's concrete
61+
SDK API is ready.
62+
63+
## Script triggers
64+
65+
Nuxt's idle-timeout, interaction, and service-worker helpers now return Unhead
66+
trigger functions. Existing `scriptOptions.trigger` usage is unchanged. Custom
67+
trigger functions may return a cleanup callback; Unhead calls it when the
68+
consumer scope is disposed.

‎package.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
"@types/jest-image-snapshot": "catalog:",
4343
"@types/leaflet": "catalog:",
4444
"@types/node": "catalog:",
45+
"@types/semver": "catalog:",
4546
"@types/youtube": "catalog:",
47+
"@unhead/vue": "catalog:",
4648
"@vue/test-utils": "catalog:",
4749
"bumpp": "catalog:",
4850
"defu": "catalog:",
@@ -62,6 +64,7 @@
6264
"typescript": "catalog:",
6365
"ufo": "catalog:",
6466
"ultrahtml": "catalog:",
67+
"unhead": "catalog:",
6568
"vitest": "catalog:",
6669
"vue": "catalog:",
6770
"vue-tsc": "catalog:"

‎packages/devtools-app/composables/rpc.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const STANDALONE_POLL_INTERVAL = 2000
3030
exportfunctionuseDevtoolsConnection(options: DevtoolsConnectionOptions={}): ()=>void{
3131
constinIframe=window.parent!==window
3232
letdisposed=false
33+
letconnectionMode: 'embedded'|'standalone'|undefined
3334
constconnectionCleanups: Array<()=>void>=[]
3435
letpollTimer: ReturnType<typeofsetInterval>|undefined
3536
letpollController: AbortController|undefined
@@ -45,6 +46,7 @@ export function useDevtoolsConnection(options: DevtoolsConnectionOptions = {}):
4546

4647
constcleanupConnection=()=>{
4748
connectionCleanups.splice(0).forEach(cleanup=>cleanup())
49+
connectionMode=undefined
4850
devtools.value=undefined
4951
appFetch.value=undefined
5052
isConnected.value=false
@@ -58,6 +60,7 @@ export function useDevtoolsConnection(options: DevtoolsConnectionOptions = {}):
5860
return
5961
stopPolling()
6062
cleanupConnection()
63+
connectionMode='embedded'
6164
isConnected.value=true
6265
// @ts-expect-error untyped
6366
appFetch.value=client.host.app.$fetch
@@ -99,10 +102,14 @@ export function useDevtoolsConnection(options: DevtoolsConnectionOptions = {}):
99102
}
100103

101104
conststopStandaloneWatch=watch(()=>standaloneUrl.value,(url)=>{
102-
// Clean up previous polling
105+
// Reconnect when the configured standalone server changes. Embedded
106+
// connections keep ownership until their host disconnects.
103107
stopPolling()
108+
if(connectionMode==='standalone')
109+
cleanupConnection()
104110

105-
if(url&&!isConnected.value){
111+
if(url&&connectionMode!=='embedded'){
112+
connectionMode='standalone'
106113
appFetch.value=ofetch.create({baseURL: url})asunknownas$Fetch
107114
// Use system color scheme preference
108115
colorMode.value=window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'

‎packages/devtools-app/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"private": true,
55
"scripts": {
66
"dev": "nuxi dev",
7+
"dev:prepare": "nuxi prepare",
78
"build": "nuxi build",
89
"generate": "nuxi generate"
910
},

‎packages/script/package.json‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"build": {
6161
"externals": [
6262
"@unhead/vue",
63+
"unhead",
6364
"@unhead/schema",
6465
"knitwork",
6566
"#build/modules/nuxt-scripts-gtm",
@@ -83,9 +84,10 @@
8384
"@types/leaflet": "^1.9.0",
8485
"@types/vimeo__player": "^2.18.3",
8586
"@types/youtube": "^0.1.0",
86-
"@unhead/vue": "^2.0.3 || ^3.0.0",
87+
"@unhead/vue": "^3.3.1",
8788
"maplibre-gl": "^5.24.0",
88-
"posthog-js": "^1.0.0"
89+
"posthog-js": "^1.0.0",
90+
"unhead": "^3.3.1"
8991
},
9092
"peerDependenciesMeta": {
9193
"@googlemaps/markerclusterer": {
@@ -115,9 +117,6 @@
115117
"@types/youtube": {
116118
"optional": true
117119
},
118-
"@unhead/vue": {
119-
"optional": true
120-
},
121120
"maplibre-gl": {
122121
"optional": true
123122
},
@@ -139,6 +138,7 @@
139138
"oxc-walker": "catalog:",
140139
"pathe": "catalog:",
141140
"pkg-types": "catalog:",
141+
"semver": "catalog:",
142142
"sirv": "catalog:",
143143
"std-env": "catalog:",
144144
"ufo": "catalog:",
@@ -152,8 +152,10 @@
152152
"@nuxt/kit": "catalog:",
153153
"@nuxt/module-builder": "catalog:",
154154
"@speedcurve/lux": "catalog:",
155+
"@unhead/vue": "catalog:",
155156
"rollup": "catalog:",
156157
"unbuild": "catalog:",
158+
"unhead": "catalog:",
157159
"unimport": "catalog:"
158160
}
159161
}

0 commit comments

Comments
 (0)