Skip to content

[🐞] Injected snippet leaks top-level const t, e, n into global scope, breaking third-party classic scripts ("Identifier 'e' has already been declared") #719

Description

@jeremycaris

What version of Partytown are you using?

@qwik.dev/partytown@0.13.2 (surfaced via @astrojs/partytown@2.1.7)

Summary

The inlined Partytown snippet declares three helper bindings at the top level of a classic <script>, so they leak into the page's shared global lexical environment and collide with unrelated third-party scripts.

The emitted snippet begins:

/* Partytown 0.13.2 - MIT QwikDev */
const t={preserveBehavior:!1},                       // default forward opts
      e=e=>{ /* resolvePartytownForwardProperty */ },
      n=Object.freeze(/* Object/Function prototype method names */);
!function(t,r,o,i,a,s,c,l,d,p,u=t,f){ /* ... */}(window, document, /* ... */);

t, e, and n are top-level consts in a classic script, so they enter the global lexical environment (shared across all classic scripts on the page). Any other classic script that top-level-declares a binding named t, e, or n — including a plain var e = ... — then throws at script-instantiation time:

Uncaught SyntaxError: Identifier 'e' has already been declared

and never executes a single line.

Impact

This silently breaks unrelated third-party embeds. In my case a commenting widget (cdn.echothread.io/widget.js) is a classic script whose minified bundle begins var e=function(e){...}. With Partytown on the page, the widget throws the SyntaxError above at load and never renders — no comments, and nothing surfaced to the user beyond a console error. Because t/e/n are among the most common minifier-chosen identifiers, the blast radius is wide: any classic third-party script that happens to top-level-declare one of these names breaks.

This is the same root cause as withastro/astro#11033 (redeclaration of const t during View Transitions). That was worked around on the Astro side (withastro/astro#11083, by not re-executing the snippet across transitions). The workaround did not touch the leak itself, so the snippet still pollutes global scope and can collide with other scripts on a single page load, as here.

Minimal reproduction

<!-- 1) Partytown's inlined snippet, reduced to the leaking declarations -->
<script>const t = {}, e = () => {}, n = [];</script>

<!-- 2) Any later classic script that top-level-declares t / e / n -->
<script>var e = function () {};</script>
<!-- => Uncaught SyntaxError: Identifier 'e' has already been declared -->

Real-world: enable @astrojs/partytown (e.g. for Google Analytics), then add any third-party classic <script> whose bundle top-level-declares var e / t / n.

Expected behavior

The snippet should not leak identifiers into the global lexical scope. Wrapping the helper declarations (t = default forward opts, e = resolvePartytownForwardProperty, n = the prototype-method list) inside the snippet's existing IIFE — or an enclosing block — would confine them and avoid colliding with third-party scripts.

Workaround for others hitting this

Load the affected third-party script as type="module". Module scope keeps its top-level var out of the global lexical environment, sidestepping the collision.

Environment

  • @qwik.dev/partytown 0.13.2 via @astrojs/partytown 2.1.7, Astro with <ClientRouter /> (View Transitions) enabled.
  • Reproduced in Chromium (desktop and mobile emulation) and on the live production site.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions