Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 92
feat: experimental nuxt/partytown support#576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
842f32fd3c0c9762595052a48e160cf3f1e2f0f2364d525e7cc9aa5a361d975File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
| 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' | ||
| @@ -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 | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // 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.') | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid escaped quotes in trigger placeholder serialization. When π οΈ 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 | ||
| } | ||
| 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 || {})) { | ||
| 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) | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // Verify our script executed in the worker (check console log) | ||
| expect(consoleLogs.some(log => log.includes('Partytown script executing in worker'))).toBe(true) | ||
| }) | ||
| }) | ||
| 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> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| <template> | ||
| <NuxtPage /> | ||
| </template> |
| 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, | ||
| }, | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "devDependencies": { | ||
| "@nuxtjs/partytown": "^2.0.0" | ||
| } | ||
| } |
| 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> |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
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 reachuseScript. Scripts tagged with partytown receive normal script tags instead oftype="text/partytown"and run on the main thread instead of in a web worker.How to reproduce:
nuxt.config.ts:.nuxt/plugins/scripts:init.tsResult: The generated code contains:
This passes the entire array as a single options parameter, with
partytownburied 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 forc[1]?.triggerwhen 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.tsto 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 inscriptOptionsfor all array-formatted registry entries.