Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
53ff96f
perf: memory leak hardening
harlan-zw Jul 15, 2026
845adb4
fix: harden lifecycle cleanup
harlan-zw Jul 16, 2026
5a4c47e
test: avoid generated devtools tsconfig dependency
harlan-zw Jul 16, 2026
f433e34
refactor: centralize abortable readiness
harlan-zw Jul 16, 2026
ae523e1
fix: isolate readiness callback failures
harlan-zw Jul 16, 2026
06960ee
fix: keep adopted readiness promises abortable
harlan-zw Jul 16, 2026
a419215
fix(script): separate app and consumer lifecycles
harlan-zw Jul 19, 2026
5e48544
fix: preserve shared useScript behavior
harlan-zw Jul 19, 2026
f0e37cc
fix(script): keep reload lifecycle live
harlan-zw Jul 19, 2026
ea1421c
fix(script): retain shared cleanup marker
harlan-zw Jul 19, 2026
a118eb1
feat!: require Nuxt 4.5 and Unhead 3.2
harlan-zw Jul 19, 2026
d2957f1
Merge remote-tracking branch 'origin/main' into feat/v2-unhead-3
harlan-zw Jul 20, 2026
fb7c5a1
fix: harden v2 script lifecycle boundaries
harlan-zw Jul 20, 2026
96afd55
test: select the TikTok loader deterministically
harlan-zw Jul 20, 2026
423bcbe
fix(proxy): cover Clarity bootstrap domains
harlan-zw Jul 20, 2026
6033168
Merge remote-tracking branch 'origin/main' into feat/v2-unhead-3
harlan-zw Jul 21, 2026
0079e2d
Merge remote-tracking branch 'origin/main' into feat/v2-unhead-3
harlan-zw Jul 21, 2026
c744044
Merge remote-tracking branch 'origin/main' into feat/v2-unhead-3
harlan-zw Jul 21, 2026
4ea62dd
Merge remote-tracking branch 'origin/main' into feat/v2-unhead-3
harlan-zw Jul 21, 2026
88a3cc2
fix: close v2 lifecycle review gaps
harlan-zw Jul 22, 2026
a877d3b
test: prepare devtools before unit tests
harlan-zw Jul 22, 2026
6131cff
fix: harden v2 lifecycle edge cases
harlan-zw Jul 22, 2026
996279a
fix: harden shared component lifecycles
harlan-zw Jul 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/content/docs/1.getting-started/2.installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ description: Install Nuxt Scripts in an existing Nuxt project.

## Quick Start

Nuxt Scripts 1.x requires Nuxt 3.16 or newer.
Nuxt Scripts 2 requires Nuxt 4.5 or newer and Unhead 3.2 or newer. Upgrade an
existing project before installing:

```bash
npx nuxi@latest upgrade --force
```

Run:

Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/1.guides/1.script-triggers.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export default defineNuxtConfig({

### User Interaction

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

::code-group

Expand Down
29 changes: 27 additions & 2 deletions docs/content/docs/3.api/1.use-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ Unhead's [complete script example](https://unhead.unjs.io/docs/head/guides/core-

Nuxt Scripts extends Unhead'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:

- `use` - The function to resolve the script.
- `resolve` - Resolve the loaded SDK with lifecycle-bound `signal` and `waitFor` helpers.
- `use` - Legacy synchronous or async SDK resolver. Prefer `resolve` for callback-based readiness.
- `trigger` - [Triggering Script Loading](/docs/guides/script-triggers)
- `bundle` - Control [first-party bundling](/docs/guides/first-party).
- `proxy` - Enable or disable supported collection proxying for a registry script.
Expand Down Expand Up @@ -119,10 +120,34 @@ Outside the Partytown path, the returned object includes:
- `proxy` - A typed proxy that queues calls until loading finishes
- `status` - Reactive ref with the script status: `'awaitingLoad'` | `'loading'` | `'loaded'` | `'error'` | `'removed'`
- `load()`{lang="ts"} - Function to manually load the script
- `remove()`{lang="ts"} - Function to remove the script from the DOM
- `signal` - An `AbortSignal` scoped to this composable consumer
- `dispose()`{lang="ts"} - Release this consumer's callbacks and triggers without removing the shared script
- `script` - The shared Unhead script instance
- `remove()`{lang="ts"} - Remove the shared script for every consumer
- `reload()`{lang="ts"} - Function to remove and reload the script (see below)
- `onLoaded()`{lang="ts"} and `onError()`{lang="ts"} - Script lifecycle callbacks

Each call receives its own consumer scope. Vue disposes that scope when its
component unmounts, while the shared script remains available to other callers.
Call `remove()`{lang="ts"} only to remove the script globally.

### Lifecycle-aware SDK readiness

Use `resolve({ waitFor })`{lang="ts"} when a vendor exposes a callback that fires
after the script element's `load` event. Listener cleanup and abort rejection are
then tied to the shared script lifecycle.

```ts
const sdk = useScript<{ ready: true }>('https://example.com/sdk.js', {
resolve: ({ waitFor }) => waitFor<{ ready: true }>((resolve) => {
window.onExampleReady = () => resolve({ ready: true })
return () => delete window.onExampleReady
}),
})

const api = await sdk.load()
```

### `reload()`{lang="ts"}

Removes the script, inserts it again, and re-executes it. Use this for a script that scans the DOM once and must scan again after SPA navigation.
Expand Down
7 changes: 5 additions & 2 deletions docs/content/docs/3.api/3.use-script-trigger-idle-timeout.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The trigger uses a timer after `onNuxtReady`; it does not wait for the browser's
## Signature

```ts
function useScriptTriggerIdleTimeout(options: IdleTimeoutScriptTriggerOptions): Promise<boolean>
function useScriptTriggerIdleTimeout(options: IdleTimeoutScriptTriggerOptions): UseScriptTrigger
```

## Arguments
Expand All @@ -31,7 +31,10 @@ export interface IdleTimeoutScriptTriggerOptions {

## Returns

A promise that resolves to `true` when the timeout completes. If the owning Vue scope is disposed after the timer starts, it stops the timer and resolves to `false`. On the server, and if the scope disappears before `onNuxtReady` runs, the promise remains pending.
An Unhead trigger function for `scriptOptions.trigger`. It starts the timer after
Nuxt is ready and loads the script when the timeout completes. Disposing the
consumer scope cancels the pending timer, including when disposal happens before
Nuxt becomes ready. The trigger does not install a timer during SSR.

## Nuxt Config Usage

Expand Down
14 changes: 8 additions & 6 deletions docs/content/docs/3.api/3.use-script-trigger-interaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ links:

Load a script when any configured interaction event occurs.

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.

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.
Listeners are attached inside `onNuxtReady`, so interactions that happen before
Nuxt is ready do not count. Passing an empty `events` array throws an error.

## Signature

```ts
function useScriptTriggerInteraction(options: InteractionScriptTriggerOptions): Promise<boolean>
function useScriptTriggerInteraction(options: InteractionScriptTriggerOptions): UseScriptTrigger
```

## Arguments
Expand All @@ -38,7 +37,10 @@ export interface InteractionScriptTriggerOptions {

## Returns

A promise that resolves to `true` after the first matching event. It resolves to `false` when `target` is null or the owning scope is disposed after the listeners have been attached. On the server, and when the scope disappears before `onNuxtReady` registers cleanup, it remains pending.
An Unhead trigger function for `scriptOptions.trigger`. It loads the script after
the first matching event, then removes every listener. Disposing the consumer
scope removes pending listeners, including when disposal happens before Nuxt
becomes ready. A `null` target leaves the script unloaded.

## Nuxt Config Usage

Expand Down Expand Up @@ -125,4 +127,4 @@ The `target` option accepts an `EventTarget` that already exists when the compos
- Listen for interactions that indicate users will need the script soon.
- Include keyboard and touch events when the feature supports those input methods.
- Use `target` to limit listeners to the relevant part of the page.
- If the script must load by a deadline even without interaction, race this trigger against a timeout in a custom promise.
- If the script must load by a deadline even without interaction, combine the event listeners and timeout in one custom trigger function.
68 changes: 68 additions & 0 deletions docs/content/docs/4.migration-guide/2.v1-to-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
title: v1 to v2
description: Migration guide for upgrading from Nuxt Scripts v1.x to v2.0.
---

Nuxt Scripts 2 moves script ownership and SDK readiness onto the lifecycle APIs
introduced in Unhead 3.2. This removes component callbacks and trigger listeners
as soon as their consumer unmounts, without tearing down a script still used by
other components.

## Requirements

| Dependency | Required version |
|---|---|
| Nuxt | `>=4.5.0` |
| `@unhead/vue` | `>=3.2.0 <4` |
| `unhead` | `>=3.2.0 <4` |

Upgrade Nuxt and refresh its locked dependencies before installing v2:

```bash
npx nuxi@latest upgrade --force
```

The module now stops setup with an actionable error when either Unhead package
is missing or outside the supported range.

## Consumer scopes

Every `useScript()`{lang="ts"} call now returns an Unhead consumer scope.
Component unmount automatically releases callbacks and trigger listeners owned
by that call.

- `dispose()`{lang="ts"} releases only the current consumer.
- `signal` aborts when you dispose that consumer or any caller removes the shared script.
- `script` points to the shared script instance.
- `remove()`{lang="ts"} still removes the shared script for all consumers.

If application code used `remove()`{lang="ts"} as component cleanup, switch it
to `dispose()`{lang="ts"}. In Vue components, manual cleanup is normally no
longer necessary.

## Custom readiness callbacks

The legacy `use` option remains supported. Callback-driven SDKs should migrate
to `resolve({ waitFor })`{lang="ts"}, which automatically removes listeners and
rejects pending readiness when the script lifecycle ends.

```diff
const script = useScript('https://example.com/sdk.js', {
- use: () => readyPromise.then(() => window.example),
+ resolve: ({ waitFor }) => waitFor((resolve) => {
+ window.onExampleReady = () => resolve(window.example)
+ return () => delete window.onExampleReady
+ }),
})
```

The bundled Google Maps, YouTube Player, Crisp, and Usercentrics integrations
now use this API. `load()`{lang="ts"} resolves only after each vendor's concrete
SDK API is ready.

## Script triggers

Nuxt's idle-timeout, interaction, and service-worker helpers now return Unhead
trigger functions. Existing `scriptOptions.trigger` usage is unchanged. Custom
trigger functions may return a cleanup callback; Unhead calls it when the
consumer scope is disposed.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
"@types/google.maps": "catalog:",
"@types/jest-image-snapshot": "catalog:",
"@types/node": "catalog:",
"@types/semver": "catalog:",
"@types/youtube": "catalog:",
"@unhead/vue": "catalog:",
"@vue/test-utils": "catalog:",
"bumpp": "catalog:",
"defu": "catalog:",
Expand All @@ -59,6 +61,7 @@
"typescript": "catalog:",
"ufo": "catalog:",
"ultrahtml": "catalog:",
"unhead": "catalog:",
"vitest": "catalog:",
"vue": "catalog:",
"vue-tsc": "catalog:"
Expand Down
Loading
Loading