diff --git a/docs/content/docs/1.getting-started/2.installation.md b/docs/content/docs/1.getting-started/2.installation.md index ae378b66..f571718d 100644 --- a/docs/content/docs/1.getting-started/2.installation.md +++ b/docs/content/docs/1.getting-started/2.installation.md @@ -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.1 or newer and Unhead 3.3.1 or newer. Upgrade an +existing project before installing: + +```bash +npx nuxi@latest upgrade --force +``` Run: diff --git a/docs/content/docs/1.guides/1.script-triggers.md b/docs/content/docs/1.guides/1.script-triggers.md index 3613f90b..ef4d679f 100644 --- a/docs/content/docs/1.guides/1.script-triggers.md +++ b/docs/content/docs/1.guides/1.script-triggers.md @@ -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 diff --git a/docs/content/docs/3.api/1.use-script.md b/docs/content/docs/3.api/1.use-script.md index f8d098a5..623250f3 100644 --- a/docs/content/docs/3.api/1.use-script.md +++ b/docs/content/docs/3.api/1.use-script.md @@ -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. @@ -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. diff --git a/docs/content/docs/3.api/3.use-script-trigger-idle-timeout.md b/docs/content/docs/3.api/3.use-script-trigger-idle-timeout.md index 31b0664c..523282c4 100644 --- a/docs/content/docs/3.api/3.use-script-trigger-idle-timeout.md +++ b/docs/content/docs/3.api/3.use-script-trigger-idle-timeout.md @@ -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 +function useScriptTriggerIdleTimeout(options: IdleTimeoutScriptTriggerOptions): UseScriptTrigger ``` ## Arguments @@ -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 diff --git a/docs/content/docs/3.api/3.use-script-trigger-interaction.md b/docs/content/docs/3.api/3.use-script-trigger-interaction.md index 838d5566..5950af2f 100644 --- a/docs/content/docs/3.api/3.use-script-trigger-interaction.md +++ b/docs/content/docs/3.api/3.use-script-trigger-interaction.md @@ -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 +function useScriptTriggerInteraction(options: InteractionScriptTriggerOptions): UseScriptTrigger ``` ## Arguments @@ -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 @@ -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. diff --git a/docs/content/docs/4.migration-guide/2.v1-to-v2.md b/docs/content/docs/4.migration-guide/2.v1-to-v2.md new file mode 100644 index 00000000..05db5087 --- /dev/null +++ b/docs/content/docs/4.migration-guide/2.v1-to-v2.md @@ -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.3.1. 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.1` | +| `@unhead/vue` | `>=3.3.1 <4` | +| `unhead` | `>=3.3.1 <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. diff --git a/package.json b/package.json index c42a6b27..15785cfb 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,9 @@ "@types/jest-image-snapshot": "catalog:", "@types/leaflet": "catalog:", "@types/node": "catalog:", + "@types/semver": "catalog:", "@types/youtube": "catalog:", + "@unhead/vue": "catalog:", "@vue/test-utils": "catalog:", "bumpp": "catalog:", "defu": "catalog:", @@ -62,6 +64,7 @@ "typescript": "catalog:", "ufo": "catalog:", "ultrahtml": "catalog:", + "unhead": "catalog:", "vitest": "catalog:", "vue": "catalog:", "vue-tsc": "catalog:" diff --git a/packages/devtools-app/composables/rpc.ts b/packages/devtools-app/composables/rpc.ts index e1272236..29dcb590 100644 --- a/packages/devtools-app/composables/rpc.ts +++ b/packages/devtools-app/composables/rpc.ts @@ -30,6 +30,7 @@ const STANDALONE_POLL_INTERVAL = 2000 export function useDevtoolsConnection(options: DevtoolsConnectionOptions = {}): () => void { const inIframe = window.parent !== window let disposed = false + let connectionMode: 'embedded' | 'standalone' | undefined const connectionCleanups: Array<() => void> = [] let pollTimer: ReturnType | undefined let pollController: AbortController | undefined @@ -45,6 +46,7 @@ export function useDevtoolsConnection(options: DevtoolsConnectionOptions = {}): const cleanupConnection = () => { connectionCleanups.splice(0).forEach(cleanup => cleanup()) + connectionMode = undefined devtools.value = undefined appFetch.value = undefined isConnected.value = false @@ -58,6 +60,7 @@ export function useDevtoolsConnection(options: DevtoolsConnectionOptions = {}): return stopPolling() cleanupConnection() + connectionMode = 'embedded' isConnected.value = true // @ts-expect-error untyped appFetch.value = client.host.app.$fetch @@ -99,10 +102,14 @@ export function useDevtoolsConnection(options: DevtoolsConnectionOptions = {}): } const stopStandaloneWatch = watch(() => standaloneUrl.value, (url) => { - // Clean up previous polling + // Reconnect when the configured standalone server changes. Embedded + // connections keep ownership until their host disconnects. stopPolling() + if (connectionMode === 'standalone') + cleanupConnection() - if (url && !isConnected.value) { + if (url && connectionMode !== 'embedded') { + connectionMode = 'standalone' appFetch.value = ofetch.create({ baseURL: url }) as unknown as $Fetch // Use system color scheme preference colorMode.value = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' diff --git a/packages/devtools-app/package.json b/packages/devtools-app/package.json index d57b8cf2..371544e5 100644 --- a/packages/devtools-app/package.json +++ b/packages/devtools-app/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "dev": "nuxi dev", + "dev:prepare": "nuxi prepare", "build": "nuxi build", "generate": "nuxi generate" }, diff --git a/packages/script/package.json b/packages/script/package.json index 2298c7e1..32962bc7 100644 --- a/packages/script/package.json +++ b/packages/script/package.json @@ -60,6 +60,7 @@ "build": { "externals": [ "@unhead/vue", + "unhead", "@unhead/schema", "knitwork", "#build/modules/nuxt-scripts-gtm", @@ -83,9 +84,10 @@ "@types/leaflet": "^1.9.0", "@types/vimeo__player": "^2.18.3", "@types/youtube": "^0.1.0", - "@unhead/vue": "^2.0.3 || ^3.0.0", + "@unhead/vue": "^3.3.1", "maplibre-gl": "^5.24.0", - "posthog-js": "^1.0.0" + "posthog-js": "^1.0.0", + "unhead": "^3.3.1" }, "peerDependenciesMeta": { "@googlemaps/markerclusterer": { @@ -115,9 +117,6 @@ "@types/youtube": { "optional": true }, - "@unhead/vue": { - "optional": true - }, "maplibre-gl": { "optional": true }, @@ -139,6 +138,7 @@ "oxc-walker": "catalog:", "pathe": "catalog:", "pkg-types": "catalog:", + "semver": "catalog:", "sirv": "catalog:", "std-env": "catalog:", "ufo": "catalog:", @@ -152,8 +152,10 @@ "@nuxt/kit": "catalog:", "@nuxt/module-builder": "catalog:", "@speedcurve/lux": "catalog:", + "@unhead/vue": "catalog:", "rollup": "catalog:", "unbuild": "catalog:", + "unhead": "catalog:", "unimport": "catalog:" } } diff --git a/packages/script/src/devtools.ts b/packages/script/src/devtools.ts index c8c4d4b4..47894a2e 100644 --- a/packages/script/src/devtools.ts +++ b/packages/script/src/devtools.ts @@ -1,7 +1,6 @@ import type { Nuxt } from '@nuxt/schema' import type { IncomingMessage, ServerResponse } from 'node:http' import type { ProxyConfig, RegistryScript } from './runtime/types' -import { Buffer } from 'node:buffer' import { existsSync } from 'node:fs' import { createResolver, extendViteConfig } from '@nuxt/kit' @@ -83,7 +82,7 @@ export function setupStandaloneApi(nuxt: Nuxt) { } if (req.method === 'POST') { - let chunks: Buffer[] = [] + const chunks: Buffer[] = [] let size = 0 let finished = false @@ -95,7 +94,7 @@ export function setupStandaloneApi(nuxt: Nuxt) { } function onAborted() { finished = true - chunks = [] + chunks.length = 0 cleanup() } function onData(chunk: Buffer) { @@ -104,7 +103,7 @@ export function setupStandaloneApi(nuxt: Nuxt) { size += chunk.byteLength if (size > DEVTOOLS_API_MAX_BODY_SIZE) { finished = true - chunks = [] + chunks.length = 0 cleanup() // Drain the remainder so the keep-alive connection can be reused. // `cleanup()` removed the normal error handler, so keep one listener @@ -123,7 +122,9 @@ export function setupStandaloneApi(nuxt: Nuxt) { finished = true cleanup() try { - const data = JSON.parse(Buffer.concat(chunks, size).toString('utf8')) + // Decode once so a multi-byte UTF-8 sequence split across chunks + // is not replaced with invalid characters before JSON parsing. + const data = JSON.parse(Buffer.concat(chunks).toString('utf8')) scriptsState = { ...data, updatedAt: Date.now() } res.statusCode = 200 res.end('ok') @@ -132,7 +133,7 @@ export function setupStandaloneApi(nuxt: Nuxt) { res.statusCode = 400 res.end('invalid json') } - chunks = [] + chunks.length = 0 } req.on('data', onData) diff --git a/packages/script/src/module.ts b/packages/script/src/module.ts index 618dd5dd..23d7c57a 100644 --- a/packages/script/src/module.ts +++ b/packages/script/src/module.ts @@ -31,8 +31,9 @@ import { hasNuxtModule, } from '@nuxt/kit' import { defu } from 'defu' -import { dirname, resolve as resolvePath_ } from 'pathe' -import { readPackageJSON, resolvePackageJSON } from 'pkg-types' +import { resolve as resolvePath_ } from 'pathe' +import { readPackageJSON } from 'pkg-types' +import { satisfies } from 'semver' import { setupPublicAssetStrategy } from './assets' import { buildDevtoolsData, buildDevtoolsEntry, setupDevtools } from './devtools' import { installNuxtModule } from './kit' @@ -47,7 +48,6 @@ import { buildProxyConfigsFromRegistry, generatePartytownResolveUrl, getPartytow import { ensureNuxtScriptsCacheStorage } from './runtime/server/utils/cache-config' import { isPublicNetworkHostname } from './runtime/server/utils/network-host' import { registerTypeTemplates, templatePlugin, templateTriggerResolver } from './templates' -import { hasUnheadSourceLessScriptLoaderFile } from './unhead-features' import { validateScriptsEnvVars } from './validate-env' export type { FirstPartyPrivacy } @@ -60,6 +60,7 @@ export type { FirstPartyPrivacy } // Matches self-closing PascalCase or kebab-case tags starting with "Script"/"script-" // e.g. or const SELF_CLOSING_SCRIPT_RE = /<((?:Script[A-Z]|script-)\w[\w-]*)\b([^>]*?)\/\s*>/g +const UNHEAD_VERSION_RANGE = '>=3.3.1 <4' /** * Expand self-closing `` component tags in page files to work around @@ -563,7 +564,7 @@ export default defineNuxtModule({ name: '@nuxt/scripts', configKey: 'scripts', compatibility: { - nuxt: '>=3.16', + nuxt: '>=4.5.1', }, }, defaults: { @@ -607,28 +608,29 @@ export default defineNuxtModule({ }) } } - // If Unhead cannot be resolved, retain the compatibility implementation. - const unheadPackagePath = await resolvePackageJSON('@unhead/vue', { - from: nuxt.options.modulesDir, - }).catch(() => { - // @unhead/vue is optional; unresolved installs use the local compatibility path. - return null - }) - const unheadPackage = unheadPackagePath - ? await readPackageJSON(unheadPackagePath).catch(() => { - // An unreadable optional peer cannot safely advertise loader support. - return null - }) - : null - const unheadVersion = unheadPackage?.version - const scriptsTypesExport = (unheadPackage?.exports as any)?.['./scripts']?.types - const scriptsTypesPath = unheadPackagePath && typeof scriptsTypesExport === 'string' - ? resolvePath_(dirname(unheadPackagePath), scriptsTypesExport) - : null - const unheadSourceLessScriptLoader = hasUnheadSourceLessScriptLoaderFile(scriptsTypesPath) - if (unheadVersion?.startsWith('1')) { - logger.error(`Nuxt Scripts requires Unhead >= 2, you are using v${unheadVersion}. Please run \`nuxi upgrade --clean\` to upgrade...`) + const [unheadVuePackage, unheadCorePackage] = await Promise.all([ + readPackageJSON('@unhead/vue', { from: nuxt.options.modulesDir }).catch(() => { + // Missing peers are reported together below with the resolved versions. + return null + }), + readPackageJSON('unhead', { from: nuxt.options.modulesDir }).catch(() => { + // Missing peers are reported together below with the resolved versions. + return null + }), + ]) + const incompatibleUnheadPackages = [ + ['@unhead/vue', unheadVuePackage?.version], + ['unhead', unheadCorePackage?.version], + ].filter(([, dependencyVersion]) => !dependencyVersion || !satisfies(dependencyVersion, UNHEAD_VERSION_RANGE)) + if (incompatibleUnheadPackages.length) { + const resolvedVersions = incompatibleUnheadPackages + .map(([dependency, dependencyVersion]) => `${dependency}=${dependencyVersion ? `v${dependencyVersion}` : 'missing'}`) + .join(', ') + throw new Error( + `[nuxt-scripts] Nuxt Scripts 2 requires @unhead/vue and unhead ${UNHEAD_VERSION_RANGE}; resolved ${resolvedVersions}. Run \`npx nuxi@latest upgrade --force\` to upgrade Nuxt and refresh its dependencies.`, + ) } + const unheadSourceLessScriptLoader = true const scripts = await registry(resolvePath) as (RegistryScript & { _importRegistered?: boolean })[] // Normalize registry entries to [input, scriptOptions?] tuple form diff --git a/packages/script/src/registry-types.json b/packages/script/src/registry-types.json index a80b376b..57c363d7 100644 --- a/packages/script/src/registry-types.json +++ b/packages/script/src/registry-types.json @@ -348,12 +348,17 @@ { "name": "MapsNamespace", "kind": "type", - "code": "type MapsNamespace = typeof window.google.maps" + "code": "type MapsNamespace = typeof google.maps" + }, + { + "name": "GoogleMapsWindow", + "kind": "type", + "code": "type GoogleMapsWindow = Window & {\n google: {\n maps: MapsNamespace & { __ib__?: () => void }\n }\n}" }, { "name": "GoogleMapsApi", "kind": "interface", - "code": "export interface GoogleMapsApi {\n maps: Promise\n}" + "code": "export interface GoogleMapsApi {\n maps: MapsNamespace\n}" }, { "name": "ScriptGoogleMapsProps", @@ -1367,7 +1372,7 @@ { "name": "YouTubePlayerApi", "kind": "interface", - "code": "export interface YouTubePlayerApi {\n YT: MaybePromise<{\n Player: YT.Player\n PlayerState: YT.PlayerState\n get: (k: string) => any\n loaded: 0 | 1\n loading: 0 | 1\n ready: (f: () => void) => void\n scan: () => void\n setConfig: (config: YT.PlayerOptions) => void\n subscribe: (\n event: EventName,\n listener: YT.Events[EventName],\n context?: any,\n ) => void\n unsubscribe: (\n event: EventName,\n listener: YT.Events[EventName],\n context?: any,\n ) => void\n }>\n}" + "code": "export interface YouTubePlayerApi {\n YT: {\n Player: YT.Player\n PlayerState: YT.PlayerState\n get: (k: string) => any\n loaded: 0 | 1\n loading: 0 | 1\n ready: (f: () => void) => void\n scan: () => void\n setConfig: (config: YT.PlayerOptions) => void\n subscribe: (\n event: EventName,\n listener: YT.Events[EventName],\n context?: any,\n ) => void\n unsubscribe: (\n event: EventName,\n listener: YT.Events[EventName],\n context?: any,\n ) => void\n }\n}" }, { "name": "ScriptYouTubePlayerProps", diff --git a/packages/script/src/registry.ts b/packages/script/src/registry.ts index f8a29e78..f0d5bbf2 100644 --- a/packages/script/src/registry.ts +++ b/packages/script/src/registry.ts @@ -638,10 +638,11 @@ export async function registry(resolve?: (path: string) => Promise): Pro // Clarity buckets visitors across letter/hash-prefixed shards (a/b/c/d/e/k/...). // Microsoft adds shards over time, so an enumerated list silently 403s // through the proxy when an unlisted letter is rolled out (#728-class bug). - // `*.clarity.ms` covers the full surface at runtime; `www.clarity.ms` is - // kept literal so the build-time URL rewrite (which filters wildcards) - // can still rewrite `https://www.clarity.ms/tag/` in bundled SDKs. - domains: ['www.clarity.ms', '*.clarity.ms'], + // `*.clarity.ms` covers the full surface at runtime. Literal hosts are + // also required because build-time URL rewriting filters wildcards; the + // bootstrap currently loads its SDK from `scripts`, uploads to `p`, and + // synchronizes consent through `c`. + domains: ['www.clarity.ms', 'scripts.clarity.ms', 'p.clarity.ms', 'c.clarity.ms', '*.clarity.ms'], privacy: PRIVACY_HEATMAP, }, partytown: { forwards: ['clarity'] }, diff --git a/packages/script/src/runtime/components/ScriptCrisp.vue b/packages/script/src/runtime/components/ScriptCrisp.vue index 23be1447..17101e5e 100644 --- a/packages/script/src/runtime/components/ScriptCrisp.vue +++ b/packages/script/src/runtime/components/ScriptCrisp.vue @@ -1,6 +1,7 @@