Skip to content

Commit

Permalink
feat: support manual script deduping (#148)
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw authored Jul 13, 2024
1 parent 4c35f73 commit 253085d
Show file tree
Hide file tree
Showing 22 changed files with 113 additions and 27 deletions.
51 changes: 43 additions & 8 deletions docs/content/docs/1.guides/1.registry-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,51 @@ of the script, so that it will never load.
You can do this by providing a `mock` value to the registry script.

```ts [nuxt.config.ts]
import { isDevelopment } from 'std-env'
export default defineNuxtConfig({
$production: {
scripts: {
registry: {
googleAnalytics: {
id: 'YOUR_ID',
},
}
}
},
})
```

### Loading multiple of the same script

You may have a setup where you need to load the same registry script multiple times
using different configuration.

By default, they will be deduped and only loaded once, to load multiple instances of the same script, you can provide a unique `key` to the script.

```ts
const { gtag, $script } = useScriptGoogleAnalytics({
id: 'G-TR58L0EF8P',
})

const { gtag: gtag2, $script: $script2 } = useScriptGoogleAnalytics({
// without a key the first script instance will be returned
key: 'gtag2',
id: 'G-1234567890',
})
```

It's important to note that when modifying the key, any environment variables you're using will break.

For example, with `gtag2` as the key, you'd need to provide runtime config as following:

```ts
export default defineNuxtConfig({
scripts: {
registry: {
googleAnalytics: isDevelopment
? 'mock' // script won't load unless manually calling load()
: {
id: 'YOUR_ID',
},
runtimeConfig: {
public: {
scripts: {
gtag2: {
id: '', // NUXT_PUBLIC_SCRIPTS_GTAG2_ID
},
},
},
},
})
Expand Down
47 changes: 47 additions & 0 deletions playground/pages/third-parties/google-analytics/multiple.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts" setup>
import { useHead, useScriptGoogleAnalytics } from '#imports'
useHead({
title: 'Google Analytics',
})
// composables return the underlying api as a proxy object and a $script with the script state
const { gtag: gtag1, $script: $script1 } = useScriptGoogleAnalytics({
key: 'gtag1',
id: 'G-TR58L0EF8P',
})
const { gtag: gtag2, $script: $script2 } = useScriptGoogleAnalytics({
key: 'gtag2',
id: 'G-1234567890',
})
// id set via nuxt scripts module config
gtag1('event', 'page_view', {
page_title: 'Google Analytics',
page_location: 'https://harlanzw.com/third-parties/google-analytics',
page_path: '/third-parties/google-analytics',
})
function triggerConversion() {
gtag2('event', 'conversion')
}
</script>

<template>
<div>
<ClientOnly>
<div>
1 status: {{ $script1.status.value }}
</div>
</ClientOnly>
<ClientOnly>
<div>
2 status: {{ $script2.status.value }}
</div>
</ClientOnly>
<button @click="triggerConversion">
Trigger Conversion
</button>
</div>
</template>
2 changes: 1 addition & 1 deletion src/runtime/registry/clarity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function useScriptClarity<T extends ClarityApi>(
_options?: ClarityInput,
) {
return useRegistryScript<T, typeof ClarityOptions>(
'clarity',
_options?.key || 'clarity',
options => ({
scriptInput: {
src: `https://www.clarity.ms/tag/${options.id}`,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/cloudflare-web-analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const CloudflareWebAnalyticsOptions = object({
export type CloudflareWebAnalyticsInput = RegistryScriptInput<typeof CloudflareWebAnalyticsOptions>

export function useScriptCloudflareWebAnalytics<T extends CloudflareWebAnalyticsApi>(_options?: CloudflareWebAnalyticsInput) {
return useRegistryScript<T, typeof CloudflareWebAnalyticsOptions>('cloudflareWebAnalytics', options => ({
return useRegistryScript<T, typeof CloudflareWebAnalyticsOptions>(_options?.key || 'cloudflareWebAnalytics', options => ({
scriptInput: {
'src': 'https://static.cloudflareinsights.com/beacon.min.js',
'data-cf-beacon': JSON.stringify({ token: options.token, spa: options.spa || true }),
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/crisp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ declare global {

export function useScriptCrisp<T extends CrispApi>(_options?: CrispInput) {
let readyPromise: Promise<void> = Promise.resolve()
return useRegistryScript<T, typeof CrispOptions>('crisp', options => ({
return useRegistryScript<T, typeof CrispOptions>(_options?.key || 'crisp', options => ({
scriptInput: {
src: 'https://client.crisp.chat/l.js', // can't be bundled
},
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/fathom-analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ declare global {
}

export function useScriptFathomAnalytics<T extends FathomAnalyticsApi>(_options?: FathomAnalyticsInput) {
return useRegistryScript<T, typeof FathomAnalyticsOptions>('fathomAnalytics', options => ({
return useRegistryScript<T, typeof FathomAnalyticsOptions>(_options?.key || 'fathomAnalytics', options => ({
scriptInput: {
src: 'https://cdn.usefathom.com/script.js', // can't be bundled
// append the data attr's
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/google-adsense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ declare global {
*/
export function useScriptGoogleAdsense<T extends GoogleAdsenseApi>(_options?: GoogleAdsenseInput) {
// Note: inputs.useScriptInput is not usable, needs to be normalized
return useRegistryScript<T, typeof GoogleAdsenseOptions>('googleAdsense', options => ({
return useRegistryScript<T, typeof GoogleAdsenseOptions>(_options?.key || 'googleAdsense', options => ({
scriptInput: {
src: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js',
},
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/google-maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ declare global {

export function useScriptGoogleMaps<T extends GoogleMapsApi>(_options?: GoogleMapsInput) {
let readyPromise: Promise<void> = Promise.resolve()
return useRegistryScript<T, typeof GoogleMapsOptions>('googleMaps', (options) => {
return useRegistryScript<T, typeof GoogleMapsOptions>(_options?.key || 'googleMaps', (options) => {
const libraries = options?.libraries || ['places']
return {
scriptInput: {
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/hotjar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const HotjarOptions = object({
export type HotjarInput = RegistryScriptInput<typeof HotjarOptions>

export function useScriptHotjar<T extends HotjarApi>(_options?: HotjarInput) {
return useRegistryScript<T, typeof HotjarOptions>('hotjar', options => ({
return useRegistryScript<T, typeof HotjarOptions>(_options?.key || 'hotjar', options => ({
scriptInput: {
src: `https://static.hotjar.com/c/hotjar-${options?.id}.js?sv=${options?.sv || 6}`,
},
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/intercom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ declare global {
}

export function useScriptIntercom<T extends IntercomApi>(_options?: IntercomInput) {
return useRegistryScript<T, typeof IntercomOptions>('intercom', options => ({
return useRegistryScript<T, typeof IntercomOptions>(_options?.key || 'intercom', options => ({
scriptInput: {
src: joinURL(`https://widget.intercom.io/widget`, options?.app_id || ''),
},
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/lemon-squeezy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ declare global {
}

export function useScriptLemonSqueezy<T extends LemonSqueezyApi>(_options?: LemonSqueezyInput) {
return useRegistryScript<T>('lemonSqueezy', () => ({
return useRegistryScript<T>(_options?.key || 'lemonSqueezy', () => ({
scriptInput: {
src: 'https://assets.lemonsqueezy.com/lemon.js',
crossorigin: false,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/matomo-analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ declare global {
}

export function useScriptMatomoAnalytics<T extends MatomoAnalyticsApi>(_options?: MatomoAnalyticsInput) {
return useRegistryScript<T, typeof MatomoAnalyticsOptions>('matomoAnalytics', options => ({
return useRegistryScript<T, typeof MatomoAnalyticsOptions>(_options?.key || 'matomoAnalytics', options => ({
scriptInput: {
src: withBase(`/matomo.js`, withHttps(options?.matomoUrl)),
crossorigin: false,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/meta-pixel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const MetaPixelOptions = object({
export type MetaPixelInput = RegistryScriptInput<typeof MetaPixelOptions>

export function useScriptMetaPixel<T extends MetaPixelApi>(_options?: MetaPixelInput) {
return useRegistryScript<T, typeof MetaPixelOptions>('metaPixel', options => ({
return useRegistryScript<T, typeof MetaPixelOptions>(_options?.key || 'metaPixel', options => ({
scriptInput: {
src: 'https://connect.facebook.net/en_US/fbevents.js',
crossorigin: false,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type NpmInput = RegistryScriptInput<typeof NpmOptions>

export function useScriptNpm<T extends Record<string | symbol, any>>(_options: NpmInput) {
// TODO support multiple providers? (e.g. jsdelivr, cdnjs, etc.) Only unpkg for now
return useRegistryScript<T, typeof NpmOptions>(`${_options.packageName}-npm`, options => ({
return useRegistryScript<T, typeof NpmOptions>(_options?.key || `${_options.packageName}-npm`, options => ({
scriptInput: {
src: withBase(options.file || '', `https://unpkg.com/${options?.packageName}@${options.version || 'latest'}`),
},
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/plausible-analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ declare global {
}

export function useScriptPlausibleAnalytics<T extends PlausibleAnalyticsApi>(_options?: PlausibleAnalyticsInput) {
return useRegistryScript<T, typeof PlausibleAnalyticsOptions>('plausibleAnalytics', (options) => {
return useRegistryScript<T, typeof PlausibleAnalyticsOptions>(_options?.key || 'plausibleAnalytics', (options) => {
const extensions = Array.isArray(options?.extension) ? options.extension.join('.') : [options?.extension]
return {
scriptInput: {
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ declare global {
const methods = ['track', 'page', 'identify', 'group', 'alias', 'reset']

export function useScriptSegment<T extends SegmentApi>(_options?: SegmentInput) {
return useRegistryScript<T, typeof SegmentOptions>('segment', (options) => {
return useRegistryScript<T, typeof SegmentOptions>(_options?.key || 'segment', (options) => {
const k = (options?.analyticsKey ?? 'analytics') as keyof Window
return {
scriptInput: {
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ declare global {
}

export function useScriptStripe<T extends StripeApi>(_options?: StripeInput) {
return useRegistryScript<T, typeof StripeOptions>('stripe', options => ({
return useRegistryScript<T, typeof StripeOptions>(_options?.key || 'stripe', options => ({
scriptInput: {
src: withQuery(
`https://js.stripe.com/v3/`,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/vimeo-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ declare global {
}

export function useScriptVimeoPlayer<T extends VimeoPlayerApi>(_options?: VimeoPlayerInput) {
const instance = useRegistryScript<T>('vimeoPlayer', () => ({
const instance = useRegistryScript<T>(_options?.key || 'vimeoPlayer', () => ({
scriptInput: {
src: 'https://player.vimeo.com/api/player.js',
},
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/x-pixel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const XPixelOptions = object({
export type XPixelInput = RegistryScriptInput<typeof XPixelOptions>

export function useScriptXPixel<T extends XPixelApi>(_options?: XPixelInput) {
return useRegistryScript<T, typeof XPixelOptions>('xPixel', (options) => {
return useRegistryScript<T, typeof XPixelOptions>(_options?.key || 'xPixel', (options) => {
return ({
scriptInput: {
src: 'https://static.ads-twitter.com/uwt.js',
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/registry/youtube-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type YouTubePlayerInput = RegistryScriptInput

export function useScriptYouTubePlayer<T extends YouTubePlayerApi>(_options: YouTubePlayerInput) {
let readyPromise: Promise<void> = Promise.resolve()
const instance = useRegistryScript<T>('youtubePlayer', () => ({
const instance = useRegistryScript<T>(_options?.key || 'youtubePlayer', () => ({
scriptInput: {
src: 'https://www.youtube.com/iframe_api',
crossorigin: false, // crossorigin can't be set or it breaks
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ const emptyOptions = object({})
export type EmptyOptionsSchema = typeof emptyOptions

export type RegistryScriptInput<T extends ObjectSchema<any, any> = EmptyOptionsSchema, Bundelable extends boolean = true> = InferInput<T> & {
/**
* A unique key to use for the script, this can be used to load multiple of the same script with different options.
*/
key?: string
scriptInput?: MaybeComputedRefEntriesOnly<Omit<ScriptBase & DataKeys & SchemaAugmentations['script'], 'src'>>
scriptOptions?: Bundelable extends true ? Omit<NuxtUseScriptOptions, 'use'> : Omit<NuxtUseScriptOptions, 'bundle' | 'use'>
}
Expand Down
2 changes: 1 addition & 1 deletion src/tpc/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function ${input.scriptFunctionName}<T extends ${input.tpcTypeImport}>(_o
$script: Promise<T> & VueScriptInstance<T>;
} {
${functionBody.join('\n')}
return useRegistryScript${hasParams ? '<T, typeof OptionSchema>' : ''}('${input.tpcKey}', options => ({
return useRegistryScript${hasParams ? '<T, typeof OptionSchema>' : ''}(_options?.key || '${input.tpcKey}', options => ({
scriptInput: {
src: withQuery('${mainScript.url}', {${mainScript.params?.map(p => `${p}: options?.${p}`)}})
},
Expand Down

0 comments on commit 253085d

Please sign in to comment.