-
Notifications
You must be signed in to change notification settings - Fork 75
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
Changes from all commits
842f32f
d3c0c97
6259505
2a48e16
0cf3f1e
2f0f236
4d525e7
cc9aa5a
361d975
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| 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) | ||
|
|
||
| // 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> |
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.