diff --git a/packages/script/src/runtime/registry/ahrefs-analytics.ts b/packages/script/src/runtime/registry/ahrefs-analytics.ts index 78444942..5b72c4f5 100644 --- a/packages/script/src/runtime/registry/ahrefs-analytics.ts +++ b/packages/script/src/runtime/registry/ahrefs-analytics.ts @@ -29,7 +29,11 @@ export interface AhrefsAnalyticsApi { } declare global { - interface Window extends AhrefsAnalyticsApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + AhrefsAnalytics: AhrefsAnalyticsApi['AhrefsAnalytics'] + } } /** diff --git a/packages/script/src/runtime/registry/calendly.ts b/packages/script/src/runtime/registry/calendly.ts index 8ad6f28b..d37e5c62 100644 --- a/packages/script/src/runtime/registry/calendly.ts +++ b/packages/script/src/runtime/registry/calendly.ts @@ -76,7 +76,11 @@ export interface CalendlyApi { } declare global { - interface Window extends CalendlyApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + Calendly: CalendlyApi['Calendly'] + } } const CALENDLY_CSS_KEY = 'nuxt-scripts-calendly-css' diff --git a/packages/script/src/runtime/registry/clarity.ts b/packages/script/src/runtime/registry/clarity.ts index 6f9d058e..b2872231 100644 --- a/packages/script/src/runtime/registry/clarity.ts +++ b/packages/script/src/runtime/registry/clarity.ts @@ -25,7 +25,11 @@ export interface ClarityApi { } declare global { - interface Window extends ClarityApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + clarity: ClarityApi['clarity'] + } } export type ClarityInput = RegistryScriptInput diff --git a/packages/script/src/runtime/registry/cloudflare-web-analytics.ts b/packages/script/src/runtime/registry/cloudflare-web-analytics.ts index 8fa5b05b..ae03fc4c 100644 --- a/packages/script/src/runtime/registry/cloudflare-web-analytics.ts +++ b/packages/script/src/runtime/registry/cloudflare-web-analytics.ts @@ -20,7 +20,11 @@ export interface CloudflareWebAnalyticsApi { } declare global { - interface Window extends CloudflareWebAnalyticsApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + __cfBeacon: CloudflareWebAnalyticsApi['__cfBeacon'] + } } export type CloudflareWebAnalyticsInput = RegistryScriptInput diff --git a/packages/script/src/runtime/registry/google-adsense.ts b/packages/script/src/runtime/registry/google-adsense.ts index 48da8661..24979a2e 100644 --- a/packages/script/src/runtime/registry/google-adsense.ts +++ b/packages/script/src/runtime/registry/google-adsense.ts @@ -15,7 +15,11 @@ export interface GoogleAdsenseApi { } declare global { - interface Window extends GoogleAdsenseApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + adsbygoogle: GoogleAdsenseApi['adsbygoogle'] + } } /** diff --git a/packages/script/src/runtime/registry/google-recaptcha.ts b/packages/script/src/runtime/registry/google-recaptcha.ts index 0ea4716e..cacd1abe 100644 --- a/packages/script/src/runtime/registry/google-recaptcha.ts +++ b/packages/script/src/runtime/registry/google-recaptcha.ts @@ -19,7 +19,11 @@ export interface GoogleRecaptchaApi { } declare global { - interface Window extends GoogleRecaptchaApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + grecaptcha: GoogleRecaptchaApi['grecaptcha'] + } } export function useScriptGoogleRecaptcha(_options?: GoogleRecaptchaInput) { diff --git a/packages/script/src/runtime/registry/google-tag-manager.ts b/packages/script/src/runtime/registry/google-tag-manager.ts index 81a16c84..d61b5b42 100644 --- a/packages/script/src/runtime/registry/google-tag-manager.ts +++ b/packages/script/src/runtime/registry/google-tag-manager.ts @@ -82,7 +82,11 @@ export interface GoogleTagManagerApi { * instead, which is also the only access path that respects a custom dataLayer name. */ declare global { - interface Window extends Pick {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + google_tag_manager: GoogleTagManagerApi['google_tag_manager'] + } } export { GoogleTagManagerOptions } diff --git a/packages/script/src/runtime/registry/hotjar.ts b/packages/script/src/runtime/registry/hotjar.ts index 9ce6dc5c..7bc6d569 100644 --- a/packages/script/src/runtime/registry/hotjar.ts +++ b/packages/script/src/runtime/registry/hotjar.ts @@ -11,7 +11,10 @@ export interface HotjarApi { } declare global { - interface Window extends HotjarApi { + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + hj: HotjarApi['hj'] _hjSettings: { hjid: number, hjsv?: number } } } diff --git a/packages/script/src/runtime/registry/intercom.ts b/packages/script/src/runtime/registry/intercom.ts index c676e0ac..789ce6f9 100644 --- a/packages/script/src/runtime/registry/intercom.ts +++ b/packages/script/src/runtime/registry/intercom.ts @@ -34,7 +34,10 @@ export interface IntercomApi { } declare global { - interface Window extends IntercomApi { + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + Intercom: IntercomApi['Intercom'] intercomSettings?: any } } diff --git a/packages/script/src/runtime/registry/leaflet.ts b/packages/script/src/runtime/registry/leaflet.ts index 6834d751..91b17ba3 100644 --- a/packages/script/src/runtime/registry/leaflet.ts +++ b/packages/script/src/runtime/registry/leaflet.ts @@ -13,7 +13,11 @@ export interface LeafletApi { } declare global { - interface Window extends LeafletApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + L: LeafletApi['L'] + } } export function useScriptLeaflet(_options?: LeafletInput) { diff --git a/packages/script/src/runtime/registry/linkedin-insight.ts b/packages/script/src/runtime/registry/linkedin-insight.ts index 2d71d72a..b594e5e7 100644 --- a/packages/script/src/runtime/registry/linkedin-insight.ts +++ b/packages/script/src/runtime/registry/linkedin-insight.ts @@ -29,7 +29,10 @@ export interface LinkedInInsightApi { } declare global { - interface Window extends LinkedInInsightApi { + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + lintrk: LinkedInInsightApi['lintrk'] _linkedin_partner_id?: string _linkedin_data_partner_ids?: string[] _linkedin_event_id?: string diff --git a/packages/script/src/runtime/registry/matomo-analytics.ts b/packages/script/src/runtime/registry/matomo-analytics.ts index 1a6429e7..180f460f 100644 --- a/packages/script/src/runtime/registry/matomo-analytics.ts +++ b/packages/script/src/runtime/registry/matomo-analytics.ts @@ -14,7 +14,11 @@ export interface MatomoAnalyticsApi { } declare global { - interface Window extends MatomoAnalyticsApi { } + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + _paq: MatomoAnalyticsApi['_paq'] + } } export interface MatomoConsent { diff --git a/packages/script/src/runtime/registry/meta-pixel.ts b/packages/script/src/runtime/registry/meta-pixel.ts index 404f89b0..c6ccdf24 100644 --- a/packages/script/src/runtime/registry/meta-pixel.ts +++ b/packages/script/src/runtime/registry/meta-pixel.ts @@ -44,7 +44,13 @@ export interface MetaPixelApi { } declare global { - interface Window extends MetaPixelApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + fbq: MetaPixelApi['fbq'] + _fbq: MetaPixelApi['_fbq'] + callMethod?: MetaPixelApi['callMethod'] + } } export { MetaPixelOptions } diff --git a/packages/script/src/runtime/registry/paypal.ts b/packages/script/src/runtime/registry/paypal.ts index 893c7028..91f2abc4 100644 --- a/packages/script/src/runtime/registry/paypal.ts +++ b/packages/script/src/runtime/registry/paypal.ts @@ -10,7 +10,10 @@ export interface PayPalApi { } declare global { - interface Window extends PayPalApi { + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + paypal: PayPalApi['paypal'] } } diff --git a/packages/script/src/runtime/registry/reddit-pixel.ts b/packages/script/src/runtime/registry/reddit-pixel.ts index fc84765c..e9a09cc9 100644 --- a/packages/script/src/runtime/registry/reddit-pixel.ts +++ b/packages/script/src/runtime/registry/reddit-pixel.ts @@ -17,7 +17,11 @@ export interface RedditPixelApi { } declare global { - interface Window extends RedditPixelApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + rdt: RedditPixelApi['rdt'] + } } export { RedditPixelOptions } diff --git a/packages/script/src/runtime/registry/segment.ts b/packages/script/src/runtime/registry/segment.ts index f0fe96ad..0cdad65d 100644 --- a/packages/script/src/runtime/registry/segment.ts +++ b/packages/script/src/runtime/registry/segment.ts @@ -31,7 +31,16 @@ interface AnalyticsApi { export type SegmentApi = Pick declare global { - interface Window extends SegmentApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + track: SegmentApi['track'] + page: SegmentApi['page'] + identify: SegmentApi['identify'] + group: SegmentApi['group'] + alias: SegmentApi['alias'] + reset: SegmentApi['reset'] + } } const methods = ['track', 'page', 'identify', 'group', 'alias', 'reset'] diff --git a/packages/script/src/runtime/registry/snapchat-pixel.ts b/packages/script/src/runtime/registry/snapchat-pixel.ts index 3c98b687..a1dd245e 100644 --- a/packages/script/src/runtime/registry/snapchat-pixel.ts +++ b/packages/script/src/runtime/registry/snapchat-pixel.ts @@ -47,7 +47,13 @@ export interface SnapPixelApi { } declare global { - interface Window extends SnapPixelApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + snaptr: SnapPixelApi['snaptr'] + _snaptr: SnapPixelApi['_snaptr'] + handleRequest?: SnapPixelApi['handleRequest'] + } } export type SnapTrPixelInput = RegistryScriptInput diff --git a/packages/script/src/runtime/registry/vimeo-player.ts b/packages/script/src/runtime/registry/vimeo-player.ts index 581675d8..502d9aee 100644 --- a/packages/script/src/runtime/registry/vimeo-player.ts +++ b/packages/script/src/runtime/registry/vimeo-player.ts @@ -15,7 +15,11 @@ export interface VimeoPlayerApi { export type VimeoPlayerInput = RegistryScriptInput declare global { - interface Window extends VimeoPlayerApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + Vimeo: VimeoPlayerApi['Vimeo'] + } } export function useScriptVimeoPlayer(_options?: VimeoPlayerInput): UseScriptContext { diff --git a/packages/script/src/runtime/registry/x-pixel.ts b/packages/script/src/runtime/registry/x-pixel.ts index 8e849d1d..3bd5c4f4 100644 --- a/packages/script/src/runtime/registry/x-pixel.ts +++ b/packages/script/src/runtime/registry/x-pixel.ts @@ -36,7 +36,11 @@ export interface XPixelApi { } declare global { - interface Window extends XPixelApi {} + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + twq: XPixelApi['twq'] + } } export { XPixelOptions } diff --git a/packages/script/src/runtime/registry/youtube-player.ts b/packages/script/src/runtime/registry/youtube-player.ts index 530ce22a..83bcbdf9 100644 --- a/packages/script/src/runtime/registry/youtube-player.ts +++ b/packages/script/src/runtime/registry/youtube-player.ts @@ -28,7 +28,10 @@ export interface YouTubePlayerApi { } declare global { - interface Window extends YouTubePlayerApi { + // Declared inline rather than via `extends`: an `extends` clause on the global `Window` + // surfaces as an unsuppressable TS2430 in consumer code when another package declares it (#852). + interface Window { + YT: YouTubePlayerApi['YT'] onYouTubeIframeAPIReady?: () => void } } diff --git a/test/types/global-window.test-d.ts b/test/types/global-window.test-d.ts index c940e36f..d7b1a593 100644 --- a/test/types/global-window.test-d.ts +++ b/test/types/global-window.test-d.ts @@ -1,4 +1,23 @@ +import type { AhrefsAnalyticsApi } from '../../packages/script/src/runtime/registry/ahrefs-analytics' +import type { CalendlyApi } from '../../packages/script/src/runtime/registry/calendly' +import type { ClarityApi } from '../../packages/script/src/runtime/registry/clarity' +import type { CloudflareWebAnalyticsApi } from '../../packages/script/src/runtime/registry/cloudflare-web-analytics' +import type { GoogleAdsenseApi } from '../../packages/script/src/runtime/registry/google-adsense' +import type { GoogleRecaptchaApi } from '../../packages/script/src/runtime/registry/google-recaptcha' import type { GoogleTagManagerApi } from '../../packages/script/src/runtime/registry/google-tag-manager' +import type { HotjarApi } from '../../packages/script/src/runtime/registry/hotjar' +import type { IntercomApi } from '../../packages/script/src/runtime/registry/intercom' +import type { LeafletApi } from '../../packages/script/src/runtime/registry/leaflet' +import type { LinkedInInsightApi } from '../../packages/script/src/runtime/registry/linkedin-insight' +import type { MatomoAnalyticsApi } from '../../packages/script/src/runtime/registry/matomo-analytics' +import type { MetaPixelApi } from '../../packages/script/src/runtime/registry/meta-pixel' +import type { PayPalApi } from '../../packages/script/src/runtime/registry/paypal' +import type { RedditPixelApi } from '../../packages/script/src/runtime/registry/reddit-pixel' +import type { SegmentApi } from '../../packages/script/src/runtime/registry/segment' +import type { SnapPixelApi } from '../../packages/script/src/runtime/registry/snapchat-pixel' +import type { VimeoPlayerApi } from '../../packages/script/src/runtime/registry/vimeo-player' +import type { XPixelApi } from '../../packages/script/src/runtime/registry/x-pixel' +import type { YouTubePlayerApi } from '../../packages/script/src/runtime/registry/youtube-player' import { describe, expectTypeOf, it } from 'vitest' /** @@ -29,3 +48,140 @@ describe('global `Window` augmentation', () => { expectTypeOf().toBeCallableWith({ event: 'test' }) }) }) + +/** + * Follow-up sweep for #852 / #855. + * + * Registry entries used to reach the global `Window` through `interface Window extends XApi {}`. + * That shape is what turns a routine member collision into an unsuppressable failure: when any + * other package declares one of the same members, the merged `Window` stops satisfying the + * `extends` clause, and TypeScript reports TS2430 at *every* `Window` augmentation in the + * program — including the consumer's own, which are nowhere near the cause and which + * `skipLibCheck` cannot silence because they are the consumer's own `.ts` files. + * + * Declaring the members inline removes the `extends` clause, so that failure mode cannot occur: + * a genuine collision now surfaces as TS2687/TS2717 on the two conflicting declarations, which + * are both in `.d.ts` files and are therefore covered by `skipLibCheck` like any other + * dependency-vs-dependency disagreement. + * + * These assertions pin the member types to their API interfaces, so the rewrite stays + * type-identical to the `extends` form it replaced and cannot silently drift. + */ +describe('registry `Window` members match their API interfaces', () => { + it('ahrefs-analytics', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('calendly', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('clarity', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('cloudflare-web-analytics', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('google-adsense', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('google-recaptcha', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('hotjar', () => { + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf<{ hjid: number, hjsv?: number }>() + }) + + it('intercom', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('leaflet', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('linkedin-insight', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('matomo-analytics', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('meta-pixel', () => { + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + }) + + it('paypal', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('reddit-pixel', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('segment', () => { + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + }) + + it('snapchat-pixel', () => { + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + }) + + it('vimeo-player', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('x-pixel', () => { + expectTypeOf().toEqualTypeOf() + }) + + it('youtube-player', () => { + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf<(() => void) | undefined>() + }) + + /** + * The inline lists above must not go stale: if an API interface gains a member, the matching + * `Window` declaration has to gain it too, or the registry's own `window.` reads stop + * typechecking. `extends` used to keep the two in step automatically. + * + * `GoogleTagManagerApi` is deliberately excluded — `dataLayer` is intentionally *not* global + * (#855), which is the one case where the two are meant to diverge. + */ + it('declares every API member on `Window`', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + }) +}) diff --git a/test/unit/global-window-augmentation.test.ts b/test/unit/global-window-augmentation.test.ts new file mode 100644 index 00000000..8d526dfe --- /dev/null +++ b/test/unit/global-window-augmentation.test.ts @@ -0,0 +1,36 @@ +import { readdirSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + +const REGISTRY_DIR = join(import.meta.dirname, '../../packages/script/src/runtime/registry') + +/** + * Guard for #852 / #855. + * + * A registry entry must not reach the global `Window` through an `extends` clause + * (`interface Window extends XApi {}`). Interface declarations merge, so as soon as any other + * package declares one of the same members — `@gtm-support/core` declaring + * `Window.dataLayer?: DataLayerObject[]` is the case that started this — the merged `Window` + * stops satisfying that clause. TypeScript then reports TS2430 at *every* `Window` augmentation + * in the program, including the consumer's own, which are nowhere near the cause and which + * `skipLibCheck` cannot silence because they live in the consumer's own `.ts` files. + * + * Declaring the members inline (`interface Window { xApi: XApi['xApi'] }`) is type-identical and + * removes that failure mode: a real collision then surfaces as TS2687/TS2717 on the two + * conflicting declarations, both of which are in `.d.ts` files and so fall under `skipLibCheck` + * like any other dependency-vs-dependency disagreement. + */ +describe('registry global `Window` augmentations', () => { + const entries = readdirSync(REGISTRY_DIR).filter(f => f.endsWith('.ts')) + + it('finds registry sources to check', () => { + expect(entries.length).toBeGreaterThan(0) + }) + + it('never augments the global `Window` via an `extends` clause', () => { + const offenders = entries.filter(file => + /\binterface\s+Window\s+extends\b/.test(readFileSync(join(REGISTRY_DIR, file), 'utf8')), + ) + expect(offenders).toEqual([]) + }) +})