Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions docs/content/docs/3.api/1.use-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ export type NuxtUseScriptOptions<T = any> = Omit<UseScriptOptions<T>, 'trigger'>
* - `false` - Do not bundle the script. (default)
*/
bundle?: boolean
/**
* [Experimental] Load the script in a web worker using Partytown.
* When enabled, adds `type="text/partytown"` to the script tag.
* Requires @nuxtjs/partytown to be installed and configured separately.
* Note: Scripts requiring DOM access (GTM, Hotjar, chat widgets) are not compatible.
* @see https://partytown.qwik.dev/
*/
partytown?: boolean
/**
* Skip any schema validation for the script input. This is useful for loading the script stubs for development without
* loading the actual script and not getting warnings.
Expand Down
55 changes: 55 additions & 0 deletions docs/content/docs/3.api/5.nuxt-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,61 @@ Global registry scripts that should be loaded.

See the [Script Registry](/scripts) for more details.

## `partytown` :badge[Experimental]{color="amber"}

- Type: `(keyof ScriptRegistry)[]`
- Default: `[]`

Registry scripts to load via [Partytown](https://partytown.qwik.dev/) (web worker).

This is a shorthand for setting `partytown: true` on individual registry scripts. When a script is listed here, it will be loaded with `type="text/partytown"` so Partytown can execute it in a web worker.

::code-group
```ts [nuxt.config.ts]
export default defineNuxtConfig({
scripts: {
partytown: ['googleAnalytics', 'plausible', 'fathom'],
registry: {
googleAnalytics: { id: 'G-XXXXX' },
plausible: { domain: 'example.com' },
fathom: { site: 'XXXXX' }
}
}
})
```
::

::callout{icon="i-heroicons-exclamation-triangle" color="amber"}
Requires [`@nuxtjs/partytown`](https://github.com/nuxt-modules/partytown) to be installed. The `forward` array is **automatically configured** for supported registry scripts.
::

### Supported Scripts

Scripts with auto-forwarding configured:
- `googleAnalytics`, `plausible`, `fathom`, `umami`, `matomo`, `segment`
- `metaPixel`, `xPixel`, `tiktokPixel`, `snapchatPixel`, `redditPixel`
- `cloudflareWebAnalytics`

### Limitations

::callout{icon="i-heroicons-x-circle" color="red"}
**Incompatible scripts** - The following cannot work with Partytown due to DOM access requirements:
- **Tag managers**: Google Tag Manager, Adobe Launch
- **Session replay**: Hotjar, Clarity, FullStory, LogRocket
- **Chat widgets**: Intercom, Crisp, Drift, HubSpot Chat
- **Video players**: YouTube, Vimeo embeds
- **A/B testing**: Optimizely, VWO, Google Optimize
::

::callout{icon="i-heroicons-exclamation-triangle" color="amber"}
**General limitations**:
- Scripts cannot directly access or modify the DOM
- `document.cookie` access requires additional Partytown configuration
- Debugging is more complex (code runs in a web worker)
- Some scripts may have timing differences compared to main thread execution
- localStorage/sessionStorage access may require forwarding configuration
::

## `defaultScriptOptions`

- Type: `NuxtUseScriptOptions`
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"prepack": "pnpm dev:prepare && nuxt-module-build build && npm run client:build",
"dev": "nuxi dev playground",
"dev:ssl": "nuxi dev playground --https",
"prepare:fixtures": "nuxi prepare test/fixtures/basic && nuxi prepare test/fixtures/cdn && nuxi prepare test/fixtures/extend-registry",
"prepare:fixtures": "nuxi prepare test/fixtures/basic && nuxi prepare test/fixtures/cdn && nuxi prepare test/fixtures/extend-registry && nuxi prepare test/fixtures/partytown",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground && pnpm run prepare:fixtures",
"typecheck": "vue-tsc --noEmit",
"bump": "bumpp package.json --commit --push --tag",
Expand Down Expand Up @@ -124,6 +124,7 @@
"valibot": "^1.2.0"
},
"devDependencies": {
"@nuxtjs/partytown": "^2.0.0",
"@nuxt/devtools-kit": "^3.1.1",
"@nuxt/devtools-ui-kit": "^3.1.1",
"@nuxt/eslint-config": "^1.12.1",
Expand Down
1,225 changes: 691 additions & 534 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,37 @@ import type {
import { NuxtScriptsCheckScripts } from './plugins/check-scripts'
import { registerTypeTemplates, templatePlugin, templateTriggerResolver } from './templates'

/**
* Partytown forward config for registry scripts.
* Scripts not listed here are likely incompatible due to DOM access requirements.
* @see https://partytown.qwik.dev/forwarding-events
*/
const PARTYTOWN_FORWARDS: Record<string, string[]> = {
googleAnalytics: ['dataLayer.push', 'gtag'],
plausible: ['plausible'],
fathom: ['fathom', 'fathom.trackEvent', 'fathom.trackPageview'],
umami: ['umami', 'umami.track'],
matomo: ['_paq.push'],
segment: ['analytics', 'analytics.track', 'analytics.page', 'analytics.identify'],
metaPixel: ['fbq'],
xPixel: ['twq'],
tiktokPixel: ['ttq.track', 'ttq.page', 'ttq.identify'],
snapchatPixel: ['snaptr'],
redditPixel: ['rdt'],
cloudflareWebAnalytics: ['__cfBeacon'],
}

export interface ModuleOptions {
/**
* The registry of supported third-party scripts. Loads the scripts in globally using the default script options.
*/
registry?: NuxtConfigScriptRegistry
/**
* Registry scripts to load via Partytown (web worker).
* Shorthand for setting `partytown: true` on individual registry scripts.
* @example ['googleAnalytics', 'plausible', 'fathom']
*/
partytown?: (keyof NuxtConfigScriptRegistry)[]
/**
* Default options for scripts.
*/
Expand Down Expand Up @@ -186,6 +212,51 @@ export default defineNuxtModule<ModuleOptions>({
)
}

// Process partytown shorthand - add partytown: true to specified registry scripts
// and auto-configure @nuxtjs/partytown forward array
if (config.partytown?.length) {
config.registry = config.registry || {}
const requiredForwards: string[] = []

for (const scriptKey of config.partytown) {
// Collect required forwards for this script
const forwards = PARTYTOWN_FORWARDS[scriptKey]
if (forwards) {
requiredForwards.push(...forwards)
}
else if (import.meta.dev) {
logger.warn(`[partytown] "${scriptKey}" has no known Partytown forwards configured. It may not work correctly or may require manual forward configuration.`)
}

const existing = config.registry[scriptKey]
if (Array.isArray(existing)) {
// [input, options] format - merge partytown into options
existing[1] = { ...existing[1], partytown: true }
}
else if (existing && typeof existing === 'object' && existing !== true && existing !== 'mock') {
// input object format - wrap with partytown option
config.registry[scriptKey] = [existing, { partytown: true }] as any
}
else if (existing === true || existing === 'mock') {
// simple enable - convert to array with partytown
config.registry[scriptKey] = [{}, { partytown: true }] as any
}
else {
// not configured - add with partytown enabled
config.registry[scriptKey] = [{}, { partytown: true }] as any
}
}

// Auto-configure @nuxtjs/partytown forward array
if (requiredForwards.length && hasNuxtModule('@nuxtjs/partytown')) {
const partytownConfig = (nuxt.options as any).partytown || {}
const existingForwards = partytownConfig.forward || []
const newForwards = [...new Set([...existingForwards, ...requiredForwards])]
;(nuxt.options as any).partytown = { ...partytownConfig, forward: newForwards }
logger.info(`[partytown] Auto-configured forwards: ${requiredForwards.join(', ')}`)
}
}
Comment on lines +215 to +258
Copy link
Contributor

Choose a reason for hiding this comment

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

The partytown feature wraps registry script entries in array format [input, {partytown: true}], but the template plugin doesn't properly handle this array format when generating initialization code, so the partytown option is never actually applied to registry scripts.

View Details
πŸ“ Patch Details
diff --git a/src/templates.ts b/src/templates.ts
index f1b2cd4..7aa6de6 100644
--- a/src/templates.ts
+++ b/src/templates.ts
@@ -110,20 +110,26 @@ export function templatePlugin(config: Partial<ModuleOptions>, registry: Require
     if (importDefinition) {
       // title case
       imports.unshift(`import { ${importDefinition.import.name} } from '${importDefinition.import.from}'`)
-      const args = (typeof c !== 'object' ? {} : c) || {}
+      let args: any = (typeof c !== 'object' ? {} : c) || {}
       if (c === 'mock') {
+        args = {}
         args.scriptOptions = { trigger: 'manual', skipValidation: true }
       }
-      else if (Array.isArray(c) && c.length === 2 && c[1]?.trigger) {
-        const triggerResolved = resolveTriggerForTemplate(c[1].trigger)
-        if (triggerResolved) {
-          args.scriptOptions = { ...c[1] } as any
-          // Store the resolved trigger as a string that will be replaced later
-          if (args.scriptOptions) {
+      else if (Array.isArray(c) && c.length === 2) {
+        // Handle array format [input, options]
+        args = typeof c[0] === 'object' ? c[0] : {}
+        const scriptOptions = c[1]
+        if (scriptOptions) {
+          const triggerResolved = scriptOptions.trigger ? resolveTriggerForTemplate(scriptOptions.trigger) : null
+          if (triggerResolved) {
+            args.scriptOptions = { ...scriptOptions } as any
             args.scriptOptions.trigger = `__TRIGGER_${triggerResolved}__` as any
+            if (triggerResolved.includes('useScriptTriggerIdleTimeout')) needsIdleTimeoutImport = true
+            if (triggerResolved.includes('useScriptTriggerInteraction')) needsInteractionImport = true
+          }
+          else {
+            args.scriptOptions = scriptOptions
           }
-          if (triggerResolved.includes('useScriptTriggerIdleTimeout')) needsIdleTimeoutImport = true
-          if (triggerResolved.includes('useScriptTriggerInteraction')) needsInteractionImport = true
         }
       }
       inits.push(`const ${k} = ${importDefinition.import.name}(${JSON.stringify(args).replace(/"__TRIGGER_(.*?)__"/g, '$1')})`)
diff --git a/test/unit/templates.test.ts b/test/unit/templates.test.ts
index fe030bb..be5bf37 100644
--- a/test/unit/templates.test.ts
+++ b/test/unit/templates.test.ts
@@ -139,7 +139,7 @@ describe('template plugin file', () => {
         },
       },
     ])
-    expect(res).toContain('useScriptStripe([{"id":"test"},{"trigger":"onNuxtReady"}])')
+    expect(res).toContain('useScriptStripe({"id":"test","scriptOptions":{"trigger":"onNuxtReady"}})')
   })
 
   it('registry with partytown option', async () => {
@@ -158,7 +158,7 @@ describe('template plugin file', () => {
         },
       },
     ])
-    expect(res).toContain('useScriptGoogleAnalytics([{"id":"G-XXXXX"},{"partytown":true}])')
+    expect(res).toContain('useScriptGoogleAnalytics({"id":"G-XXXXX","scriptOptions":{"partytown":true}})')
   })
 
   // Test idleTimeout trigger in globals
@@ -203,8 +203,9 @@ describe('template plugin file', () => {
         },
       },
     ])
-    // Registry scripts pass trigger objects directly, they don't resolve triggers in templates
-    expect(res).toContain('useScriptGoogleAnalytics([{"id":"GA_MEASUREMENT_ID"},{"trigger":{"idleTimeout":5000}}])')
+    // Registry scripts now properly handle triggers in scriptOptions, including resolving idleTimeout/interaction triggers
+    expect(res).toContain('useScriptTriggerIdleTimeout({ timeout: 5000 })')
+    expect(res).toContain('useScriptGoogleAnalytics({"id":"GA_MEASUREMENT_ID","scriptOptions":{"trigger":useScriptTriggerIdleTimeout({ timeout: 5000 })}}')
   })
 
   // Test both triggers together (should import both)

Analysis

Registry scripts with partytown option not properly handled in template generation

What fails: When using scripts.partytown: ['googleAnalytics'] to enable partytown for registry scripts, the generated template code doesn't properly unpack the array format [input, {partytown: true}], causing the partytown option to never reach useScript. Scripts tagged with partytown receive normal script tags instead of type="text/partytown" and run on the main thread instead of in a web worker.

How to reproduce:

  1. Set config in nuxt.config.ts:
export default defineNuxtConfig({
  scripts: {
    partytown: ['googleAnalytics'],
    registry: {
      googleAnalytics: { id: 'G-XXXXX' }
    }
  }
})
  1. Build/generate the project
  2. Inspect the generated template in .nuxt/plugins/scripts:init.ts

Result: The generated code contains:

const googleAnalytics = useScriptGoogleAnalytics([{id:"G-XXXXX"},{partytown:true}])

This passes the entire array as a single options parameter, with partytown buried at array index [1].partytown, making it inaccessible to the registry function.

Expected: The partytown option should be unpacked into scriptOptions:

 )

Root cause: The template plugin in src/templates.ts (lines 108-131) only checked for c[1]?.trigger when handling array format for registry scripts. When partytown is the only option in the second element, the condition failed and the entire array was treated as a single parameter, unlike how globals handle the same array format correctly (lines 136-147).

Fix: Updated registry script handling in src/templates.ts to properly unpack array format [input, options] regardless of whether options contain a trigger, similar to the existing globals handling. The second element is now correctly placed in scriptOptions for all array-formatted registry entries.


const composables = [
'useScript',
'useScriptEventPage',
Expand Down
29 changes: 27 additions & 2 deletions src/runtime/composables/useScript.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { UseScriptInput, UseScriptOptions, VueScriptInstance } from '@unhead/vue/scripts'
import { defu } from 'defu'
import { useScript as _useScript } from '@unhead/vue/scripts'
import { reactive } from 'vue'
import { reactive, ref } from 'vue'
import type { NuxtDevToolsScriptInstance, NuxtUseScriptOptions, UseFunctionType, UseScriptContext } from '../types'
import { onNuxtReady, useNuxtApp, useRuntimeConfig, injectHead } from 'nuxt/app'
import { onNuxtReady, useNuxtApp, useRuntimeConfig, injectHead, useHead } from 'nuxt/app'
import { logger } from '../logger'
// @ts-expect-error virtual template
import { resolveTrigger } from '#build/nuxt-scripts-trigger-resolver'
Expand All @@ -22,6 +22,31 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
input = typeof input === 'string' ? { src: input } : input
options = defu(options, useNuxtScriptRuntimeConfig()?.defaultScriptOptions) as NuxtUseScriptOptions<T>

// Partytown quick-path: use useHead for SSR rendering
// Partytown needs scripts in initial HTML with type="text/partytown"
if (options.partytown) {
const src = input.src
if (!src) {
throw new Error('useScript with partytown requires a src')
}
useHead({
script: [{ src, type: 'text/partytown' }],
})
// Register with nuxtApp.$scripts for DevTools visibility
const nuxtApp = useNuxtApp()
nuxtApp.$scripts = nuxtApp.$scripts! || reactive({})
const status = ref('loaded')
const stub = {
id: src,
status,
load: () => Promise.resolve({} as T),
remove: () => false,
entry: undefined,
} as any as UseScriptContext<UseFunctionType<NuxtUseScriptOptions<T>, T>>
nuxtApp.$scripts[src] = stub
return stub
}

// Warn about unsupported bundling for dynamic sources (internal value set by transform)
if (import.meta.dev && (options.bundle as any) === 'unsupported') {
console.warn('[Nuxt Scripts] Bundling is not supported for dynamic script sources. Static URLs are required for bundling.')
Expand Down
7 changes: 7 additions & 0 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ export type NuxtUseScriptOptions<T extends Record<symbol | string, any> = {}> =
* Note: Using 'force' may significantly increase build time as scripts will be re-downloaded on every build.
*/
bundle?: boolean | 'force'
/**
* Load the script in a web worker using Partytown.
* When enabled, adds `type="text/partytown"` to the script tag.
* Requires @nuxtjs/partytown to be installed and configured separately.
* @see https://partytown.qwik.dev/
*/
partytown?: boolean
/**
* Skip any schema validation for the script input. This is useful for loading the script stubs for development without
* loading the actual script and not getting warnings.
Expand Down
23 changes: 13 additions & 10 deletions src/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,23 +110,26 @@ export function templatePlugin(config: Partial<ModuleOptions>, registry: Require
if (importDefinition) {
// title case
imports.unshift(`import { ${importDefinition.import.name} } from '${importDefinition.import.from}'`)
const args = (typeof c !== 'object' ? {} : c) || {}
if (c === 'mock') {
args.scriptOptions = { trigger: 'manual', skipValidation: true }
inits.push(`const ${k} = ${importDefinition.import.name}({ scriptOptions: { trigger: 'manual', skipValidation: true } })`)
}
else if (Array.isArray(c) && c.length === 2 && c[1]?.trigger) {
const triggerResolved = resolveTriggerForTemplate(c[1].trigger)
else if (Array.isArray(c) && c.length === 2) {
// [input, options] format - unpack properly
const input = c[0] || {}
const scriptOptions = { ...c[1] }
const triggerResolved = resolveTriggerForTemplate(scriptOptions?.trigger)
if (triggerResolved) {
args.scriptOptions = { ...c[1] } as any
// Store the resolved trigger as a string that will be replaced later
if (args.scriptOptions) {
args.scriptOptions.trigger = `__TRIGGER_${triggerResolved}__` as any
}
scriptOptions.trigger = `__TRIGGER_${triggerResolved}__` as any
if (triggerResolved.includes('useScriptTriggerIdleTimeout')) needsIdleTimeoutImport = true
if (triggerResolved.includes('useScriptTriggerInteraction')) needsInteractionImport = true
}
const args = { ...input, scriptOptions }
inits.push(`const ${k} = ${importDefinition.import.name}(${JSON.stringify(args).replace(/"__TRIGGER_(.*?)__"/g, '$1')})`)
Comment on lines +116 to +127
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid escaped quotes in trigger placeholder serialization.

When trigger.interaction is used, resolveTriggerForTemplate returns a string containing JSON with double quotes. Embedding that in the placeholder and then JSON.stringify causes escaped quotes (\") to remain after replacement, producing invalid JS (e.g., events: [\"click\"]). Use a fixed placeholder and inject the raw expression after stringifying.

πŸ› οΈ Suggested fix
-        const scriptOptions = { ...c[1] }
-        const triggerResolved = resolveTriggerForTemplate(scriptOptions?.trigger)
+        const scriptOptions = { ...c[1] }
+        let triggerResolved: string | null = null
+        let triggerPlaceholder: string | null = null
+        triggerResolved = resolveTriggerForTemplate(scriptOptions?.trigger)
         if (triggerResolved) {
-          scriptOptions.trigger = `__TRIGGER_${triggerResolved}__` as any
+          triggerPlaceholder = '__TRIGGER__'
+          scriptOptions.trigger = triggerPlaceholder as any
           if (triggerResolved.includes('useScriptTriggerIdleTimeout')) needsIdleTimeoutImport = true
           if (triggerResolved.includes('useScriptTriggerInteraction')) needsInteractionImport = true
         }
         const args = { ...input, scriptOptions }
-        inits.push(`const ${k} = ${importDefinition.import.name}(${JSON.stringify(args).replace(/"__TRIGGER_(.*?)__"/g, '$1')})`)
+        let argsString = JSON.stringify(args)
+        if (triggerResolved && triggerPlaceholder) {
+          argsString = argsString.replace(`"${triggerPlaceholder}"`, triggerResolved)
+        }
+        inits.push(`const ${k} = ${importDefinition.import.name}(${argsString})`)
πŸ€– Prompt for AI Agents
In `@src/templates.ts` around lines 116 - 127, The code embeds triggerResolved
into scriptOptions.trigger as a templated value which then gets
JSON.stringify'd, producing escaped quotes when triggerResolved contains JSON
(e.g., trigger.interaction), so change the approach: set scriptOptions.trigger
to a fixed placeholder token (e.g., "__TRIGGER_PLACEHOLDER__") instead of
`__TRIGGER_${triggerResolved}__`, keep the import flags
(needsIdleTimeoutImport/needsInteractionImport) based on triggerResolved as you
already do, then after building the JSON string for args in the inits.push call,
replace the quoted placeholder string with the raw triggerResolved expression
(i.e., JSON.stringify(args).replace(/"__TRIGGER_PLACEHOLDER__"/g,
triggerResolved)) so the final emitted JS injects the unescaped trigger object;
update the line that sets scriptOptions.trigger and the replacement in the
inits.push accordingly (refer to resolveTriggerForTemplate,
needsIdleTimeoutImport, needsInteractionImport, and the inits.push constructing
const ${k} = ${importDefinition.import.name}(...)).

}
else {
const args = (typeof c !== 'object' ? {} : c) || {}
inits.push(`const ${k} = ${importDefinition.import.name}(${JSON.stringify(args)})`)
}
inits.push(`const ${k} = ${importDefinition.import.name}(${JSON.stringify(args).replace(/"__TRIGGER_(.*?)__"/g, '$1')})`)
}
}
for (const [k, c] of Object.entries(config.globals || {})) {
Expand Down
10 changes: 10 additions & 0 deletions test/e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,16 @@ describe('basic', () => {
const text = await page.$eval('#script-src', el => el.textContent)
expect(text).toMatchInlineSnapshot(`"/_scripts/6bEy8slcRmYcRT4E2QbQZ1CMyWw9PpHA7L87BtvSs2U.js"`)
})
it('partytown adds type attribute', async () => {
const { page } = await createPage('/partytown')
await page.waitForTimeout(500)
// verify the script tag has type="text/partytown"
const scriptType = await page.evaluate(() => {
const script = document.querySelector('script[src="/myScript.js"]')
return script?.getAttribute('type')
})
expect(scriptType).toBe('text/partytown')
})
})

describe('youtube', () => {
Expand Down
53 changes: 53 additions & 0 deletions test/e2e/partytown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest'
import { createResolver } from '@nuxt/kit'
import { getBrowser, url, waitForHydration, setup } from '@nuxt/test-utils/e2e'

const { resolve } = createResolver(import.meta.url)

await setup({
rootDir: resolve('../fixtures/partytown'),
browser: true,
})

describe('partytown integration', () => {
it('script tag has type="text/partytown" when partytown option is enabled', async () => {
const browser = await getBrowser()
const page = await browser.newPage()

await page.goto(url('/'), { waitUntil: 'networkidle' })
await waitForHydration(page, '/')

// Verify our module correctly sets the type attribute for partytown
// Note: Partytown changes type to "text/partytown-x" after processing
const scriptType = await page.evaluate(() => {
const script = document.querySelector('script[src="/worker-script.js"]')
return script?.getAttribute('type')
})
expect(scriptType?.startsWith('text/partytown')).toBe(true)
})

it('partytown library is loaded and script executes in worker', async () => {
const browser = await getBrowser()
const page = await browser.newPage()

// Capture console messages to verify worker execution
const consoleLogs: string[] = []
page.on('console', msg => consoleLogs.push(msg.text()))

await page.goto(url('/'), { waitUntil: 'networkidle' })
await waitForHydration(page, '/')

// Wait for partytown to execute scripts
await page.waitForTimeout(1000)

// Verify partytown library is loaded
const partytownLib = await page.evaluate(() => {
const scripts = Array.from(document.querySelectorAll('script'))
return scripts.some(s => s.id === 'partytown' || s.src?.includes('partytown'))
})
expect(partytownLib).toBe(true)

// Verify our script executed in the worker (check console log)
expect(consoleLogs.some(log => log.includes('Partytown script executing in worker'))).toBe(true)
})
})
9 changes: 9 additions & 0 deletions test/fixtures/basic/pages/partytown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script setup lang="ts">
useScript('/myScript.js', { partytown: true })
</script>

<template>
<div id="partytown-test">
Partytown test page
</div>
</template>
3 changes: 3 additions & 0 deletions test/fixtures/partytown/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<NuxtPage />
</template>
14 changes: 14 additions & 0 deletions test/fixtures/partytown/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineNuxtConfig } from 'nuxt/config'

export default defineNuxtConfig({
modules: [
'@nuxtjs/partytown',
'@nuxt/scripts',
],

compatibilityDate: '2024-07-05',

partytown: {
debug: true,
},
})
5 changes: 5 additions & 0 deletions test/fixtures/partytown/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"devDependencies": {
"@nuxtjs/partytown": "^2.0.0"
}
}
13 changes: 13 additions & 0 deletions test/fixtures/partytown/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup lang="ts">
// Load script in partytown web worker
useScript('/worker-script.js', { partytown: true })
</script>

<template>
<div>
<h1>Partytown Test</h1>
<div id="status">
Ready
</div>
</div>
</template>
Loading
Loading