Skip to content

Commit

Permalink
feat: write integration ScriptRegistry types
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw committed Apr 3, 2024
1 parent eae1e13 commit c3db3e4
Show file tree
Hide file tree
Showing 13 changed files with 156 additions and 43 deletions.
34 changes: 29 additions & 5 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
import { createResolver } from '@nuxt/kit'
import type { ScriptRegistry } from '#nuxt-scripts'

const { resolve } = createResolver(import.meta.url)

const scripts: ScriptRegistry = {
myCustomScript: [
{
id: '123',
},
{
assetStrategy: 'bundle',
},
],
confetti: {
version: 'latest',
},
}

export default defineNuxtConfig({
modules: [
'@nuxt/scripts',
Expand All @@ -6,11 +25,16 @@ export default defineNuxtConfig({
],
devtools: { enabled: true },
scripts: {
register: {
confetti: {
version: 'latest',
},
},
register: scripts,
// TODO globals / register / overrides
},
hooks: {
'scripts:registry': function (registry) {
registry.push({
name: 'useScriptCustom',
key: 'myCustomScript',
from: resolve('./scripts/myCustomScript'),
})
},
},
})
20 changes: 20 additions & 0 deletions playground/pages/third-parties/google-analytics.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<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 { $script } = useScriptGoogleAnalytics({ id: 'KBXOGxgqMFjm2mxtJDJg0iDn5AnGYb9C' })
</script>

<template>
<div>
<ClientOnly>
<div>
status: {{ $script.status.value }}
</div>
</ClientOnly>
</div>
</template>
30 changes: 30 additions & 0 deletions playground/scripts/myCustomScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { type Input, object, string } from 'valibot'
import { useScript } from '#imports'
import type { NuxtUseScriptOptions } from '#nuxt-scripts'

export interface MyCustomScriptApi {
}

export const MyCustomScriptOptions = object({
id: string(),
})

export type MyCustomScriptInput = Input<typeof MyCustomScriptOptions>

declare global {
interface Window {
customScript: MyCustomScriptApi
}
}

export function useScriptCustom<T extends MyCustomScriptApi>(options?: MyCustomScriptInput, _scriptOptions?: Omit<NuxtUseScriptOptions<T>, 'beforeInit' | 'use'>) {
const scriptOptions: NuxtUseScriptOptions<T> = _scriptOptions || {}
return useScript<MyCustomScriptApi>({
key: 'myCustomScript',
src: 'https://example.com/script.js',
...options,
}, {
...scriptOptions,
use: () => window.fathom,
})
}
22 changes: 21 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,30 @@ export default defineNuxtModule<ModuleOptions>({
// @ts-expect-error runtime
await nuxt.hooks.callHook('scripts:registry', registry)

// augment types to support the integrations registry
extendTypes(name!, async ({ typesPath }) => {
return `
declare module '#app' {
interface NuxtApp {
${nuxt.options.dev ? `_scripts: (import('#nuxt-scripts').NuxtAppScript)[]` : ''}
}
}
declare module '#nuxt-scripts' {
type NuxtUseScriptOptions = Omit<import('${typesPath}').NuxtUseScriptOptions, 'use' | 'beforeInit'>
interface ScriptRegistry {
${registry.filter(i => i.key && i.module !== '@nuxt/scripts').map((i) => {
const ucFirstKey = i.key!.substring(0, 1).toUpperCase() + i.key!.substring(1)
return ` ${i.key}?: import('${i.from}').${ucFirstKey}Input | [import('${i.from}').${ucFirstKey}Input, NuxtUseScriptOptions]`
}).join('\n')}
}
}
`
})

if (config.globals?.length || Object.keys(config.register || {}).length) {
// create a virtual plugin
const template = addTemplate({
filename: `modules/${name}.mjs`,
filename: `modules/${name!.replace('/', '-')}.mjs`,
write: true,
getContents() {
const imports = ['useScript', 'defineNuxtPlugin']
Expand Down
11 changes: 8 additions & 3 deletions src/plugins/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function NuxtScriptAssetBundlerTransformer(options: AssetBundlerTransform
const node = _node as SimpleCallExpression
let scriptKey: string | undefined
let scriptSrcNode: any | undefined
let src: string | undefined
let src: false | string | undefined
if (fnName === 'useScript') {
// do easy case first where first argument is a literal
if (node.arguments[0].type === 'Literal') {
Expand Down Expand Up @@ -103,10 +103,15 @@ export function NuxtScriptAssetBundlerTransformer(options: AssetBundlerTransform
const srcProperty = node.arguments[0].properties.find(
(p: any) => p.key?.name === 'src' || p.key?.value === 'src',
)
if (srcProperty?.value?.value)
if (srcProperty?.value?.value) {
scriptSrcNode = srcProperty.value
else
}
else {
src = registryNode.src || registryNode.transform?.(fnArg0 as any as Input<any>)
// not supported
if (src === false)
return
}
scriptKey = registryNode.key
}
}
Expand Down
7 changes: 5 additions & 2 deletions src/runtime/registry/cloudflare-web-analytics.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type Input, boolean, minLength, object, optional, string } from 'valibot'
import { defu } from 'defu'
import { useScript, validateScriptInputSchema } from '#imports'
import type { NuxtUseScriptOptions } from '#nuxt-scripts'
import type { NuxtUseScriptIntegrationOptions, NuxtUseScriptOptions } from '#nuxt-scripts'

export interface CloudflareWebAnalyticsApi {
__cfBeacon: {
Expand Down Expand Up @@ -33,10 +33,13 @@ export const CloudflareWebAnalyticsOptions = object({
spa: optional(boolean()),
})

export function useScriptCloudflareWebAnalytics<T extends CloudflareWebAnalyticsApi>(options?: Input<typeof CloudflareWebAnalyticsOptions>, _scriptOptions?: Omit<NuxtUseScriptOptions<T>, 'beforeInit' | 'use'>) {
export type CloudflareWebAnalyticsInput = Input<typeof CloudflareWebAnalyticsOptions>

export function useScriptCloudflareWebAnalytics<T extends CloudflareWebAnalyticsApi>(options?: CloudflareWebAnalyticsInput, _scriptOptions?: NuxtUseScriptIntegrationOptions) {
const scriptOptions: NuxtUseScriptOptions<T> = _scriptOptions || {}
scriptOptions.beforeInit = () => {
import.meta.dev && validateScriptInputSchema(CloudflareWebAnalyticsOptions, options)
scriptOptions.beforeInit?.()
}
return useScript<CloudflareWebAnalyticsApi>({
'src': 'https://static.cloudflareinsights.com/beacon.min.js',
Expand Down
13 changes: 7 additions & 6 deletions src/runtime/registry/confetti.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { type Input, pick } from 'valibot'
import { NpmOptions, useScriptNpm } from './npm'
import type { NuxtUseScriptOptions } from '#nuxt-scripts'
import type { NuxtUseScriptIntegrationOptions } from '#nuxt-scripts'

export interface JSConfettiApi {
export interface ConfettiApi {
addConfetti: (options?: { emojis: string[] }) => void
}

declare global {
interface Window {
JSConfetti: { new (): JSConfettiApi }
JSConfetti: { new (): ConfettiApi }
}
}

export const JSConfettiOptions = pick(NpmOptions, ['version'])
export const ConfettiOptions = pick(NpmOptions, ['version'])
export type ConfettiInput = Input<typeof ConfettiOptions>

export function useScriptConfetti<T extends JSConfettiApi>(options: Input<typeof JSConfettiOptions>, _scriptOptions: NuxtUseScriptOptions<T> = {}) {
return useScriptNpm<T>({
export function useScriptConfetti(options: ConfettiInput, _scriptOptions: NuxtUseScriptIntegrationOptions = {}) {
return useScriptNpm<ConfettiApi>({
packageName: 'js-confetti',
version: options.version,
file: 'dist/js-confetti.browser.js',
Expand Down
6 changes: 4 additions & 2 deletions src/runtime/registry/facebook-pixel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Input, number, object, string, union } from 'valibot'
import { useScript, validateScriptInputSchema } from '#imports'
import type { NuxtUseScriptOptions } from '#nuxt-scripts'
import type { NuxtUseScriptIntegrationOptions, NuxtUseScriptOptions } from '#nuxt-scripts'

type StandardEvents = 'AddPaymentInfo' | 'AddToCart' | 'AddToWishlist' | 'CompleteRegistration' | 'Contact' | 'CustomizeProduct' | 'Donate' | 'FindLocation' | 'InitiateCheckout' | 'Lead' | 'Purchase' | 'Schedule' | 'Search' | 'StartTrial' | 'SubmitApplication' | 'Subscribe' | 'ViewContent'
interface EventObjectProperties {
Expand Down Expand Up @@ -39,8 +39,9 @@ declare global {
export const FacebookPixelOptions = object({
id: union([string(), number()]),
})
export type FacebookPixelInput = Input<typeof FacebookPixelOptions>

export function useScriptFacebookPixel<T extends FacebookPixelApi>(options?: Input<typeof FacebookPixelOptions>, _scriptOptions?: Omit<NuxtUseScriptOptions<T>, 'beforeInit' | 'use'>) {
export function useScriptFacebookPixel<T extends FacebookPixelApi>(options?: FacebookPixelInput, _scriptOptions?: NuxtUseScriptIntegrationOptions) {
const scriptOptions: NuxtUseScriptOptions<T> = _scriptOptions || {}
scriptOptions.beforeInit = () => {
import.meta.dev && validateScriptInputSchema(FacebookPixelOptions, options)
Expand All @@ -60,6 +61,7 @@ export function useScriptFacebookPixel<T extends FacebookPixelApi>(options?: Inp
fbq('init', options?.id)
fbq('track', 'PageView')
}
scriptOptions.beforeInit?.()
}
return useScript<T>({
key: 'facebookPixel',
Expand Down
7 changes: 5 additions & 2 deletions src/runtime/registry/fathom-analytics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Input, boolean, literal, object, optional, string, union } from 'valibot'
import { useScript, validateScriptInputSchema } from '#imports'
import type { NuxtUseScriptOptions } from '#nuxt-scripts'
import type { NuxtUseScriptIntegrationOptions, NuxtUseScriptOptions } from '#nuxt-scripts'

export const FathomAnalyticsOptions = object({
'site': string(), // site is required
Expand All @@ -11,6 +11,8 @@ export const FathomAnalyticsOptions = object({
'data-honor-dnt': optional(boolean()),
})

export type FathomAnalyticsInput = Input<typeof FathomAnalyticsOptions>

export interface FathomAnalyticsApi {
trackPageview: (ctx?: { url: string, referrer?: string }) => void
trackEvent: (eventName: string, value: { _value: number }) => void
Expand All @@ -22,10 +24,11 @@ declare global {
}
}

export function useScriptFathomAnalytics<T extends FathomAnalyticsApi>(options?: Input<typeof FathomAnalyticsOptions>, _scriptOptions?: Omit<NuxtUseScriptOptions<T>, 'beforeInit' | 'use'>) {
export function useScriptFathomAnalytics<T extends FathomAnalyticsApi>(options?: FathomAnalyticsInput, _scriptOptions?: Omit<NuxtUseScriptIntegrationOptions, 'assetStrategy'>) {
const scriptOptions: NuxtUseScriptOptions<T> = _scriptOptions || {}
scriptOptions.beforeInit = () => {
import.meta.dev && validateScriptInputSchema(FathomAnalyticsOptions, options)
scriptOptions.beforeInit?.()
}
return useScript<FathomAnalyticsApi>({
src: 'https://cdn.usefathom.com/script.js',
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/registry/hotjar.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { number, object, optional } from 'valibot'
import { useScript, validateScriptInputSchema } from '#imports'
import type { NuxtUseScriptOptions, ScriptDynamicSrcInput } from '#nuxt-scripts'
import type { NuxtUseScriptIntegrationOptions, NuxtUseScriptOptions, ScriptDynamicSrcInput } from '#nuxt-scripts'

export interface HotjarApi {
hj: ((event: 'identify', userId: string, attributes?: Record<string, any>) => void) & ((event: 'stateChange', path: string) => void) & ((event: 'event', eventName: string) => void) & ((event: string, arg?: string) => void) & ((...params: any[]) => void) & {
Expand All @@ -21,7 +21,7 @@ export const HotjarOptions = object({

export type HotjarInput = ScriptDynamicSrcInput<typeof HotjarOptions>

export function useScriptHotjar<T extends HotjarApi>(options?: HotjarInput, _scriptOptions?: Omit<NuxtUseScriptOptions<T>, 'assetStrategy' | 'beforeInit' | 'use'>) {
export function useScriptHotjar<T extends HotjarApi>(options?: HotjarInput, _scriptOptions?: NuxtUseScriptIntegrationOptions) {
const scriptOptions: NuxtUseScriptOptions<T> = _scriptOptions || {}
scriptOptions.beforeInit = () => {
import.meta.dev && validateScriptInputSchema(HotjarOptions, options)
Expand All @@ -32,6 +32,7 @@ export function useScriptHotjar<T extends HotjarApi>(options?: HotjarInput, _scr
(window.hj.q = window.hj.q || []).push(params)
}
}
scriptOptions.beforeInit?.()
}
return useScript<T>({
key: 'hotjar',
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/registry/intercom.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type Input, literal, number, object, optional, string, union } from 'valibot'
import { joinURL } from 'ufo'
import { useScript, validateScriptInputSchema } from '#imports'
import type { NuxtUseScriptOptions, ScriptDynamicSrcInput } from '#nuxt-scripts'
import type { NuxtUseScriptIntegrationOptions, NuxtUseScriptOptions, ScriptDynamicSrcInput } from '#nuxt-scripts'

export const IntercomOptions = object({
app_id: string(),
Expand Down Expand Up @@ -48,13 +48,14 @@ declare global {
}
}

export function useScriptIntercom<T extends IntercomApi>(options?: IntercomInput, _scriptOptions?: Omit<NuxtUseScriptOptions<T>, 'beforeInit' | 'use'>) {
export function useScriptIntercom<T extends IntercomApi>(options?: IntercomInput, _scriptOptions?: NuxtUseScriptIntegrationOptions) {
const scriptOptions: NuxtUseScriptOptions<T> = _scriptOptions || {}
scriptOptions.beforeInit = () => {
import.meta.dev && validateScriptInputSchema(IntercomOptions, options)
// we need to insert the hj function
if (import.meta.client)
window.intercomSettings = options
scriptOptions.beforeInit?.()
}
return useScript<T>({
key: 'intercom',
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/registry/segment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { object, optional, string } from 'valibot'
import { useScript, validateScriptInputSchema } from '#imports'
import type { NuxtUseScriptOptions, ScriptDynamicSrcInput } from '#nuxt-scripts'
import type { NuxtUseScriptIntegrationOptions, NuxtUseScriptOptions, ScriptDynamicSrcInput } from '#nuxt-scripts'

export const SegmentOptions = object({
writeKey: string(),
Expand All @@ -24,7 +24,7 @@ declare global {
interface Window extends SegmentApi {}
}

export function useScriptSegment<T extends SegmentApi>(options?: SegmentInput, _scriptOptions?: Omit<NuxtUseScriptOptions<T>, 'beforeInit' | 'use'>) {
export function useScriptSegment<T extends SegmentApi>(options?: SegmentInput, _scriptOptions?: NuxtUseScriptIntegrationOptions) {
const scriptOptions: NuxtUseScriptOptions<T> = _scriptOptions || {}
scriptOptions.beforeInit = () => {
import.meta.dev && validateScriptInputSchema(SegmentOptions, options)
Expand All @@ -45,6 +45,7 @@ export function useScriptSegment<T extends SegmentApi>(options?: SegmentInput, _
}
window.analytics.page()
}
scriptOptions.beforeInit?.()
}
const analyticsKey = options?.analyticsKey || 'analytics'
return useScript<T>({
Expand Down
34 changes: 18 additions & 16 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import type { UseScriptInput, VueScriptInstance } from '@unhead/vue'
import type { ComputedRef, Ref } from 'vue'
import type { Input, ObjectSchema } from 'valibot'
import type { Import } from 'unimport'
import type { SegmentOptions } from './registry/segment'
import type { CloudflareWebAnalyticsOptions } from './registry/cloudflare-web-analytics'
import type { FacebookPixelOptions } from './registry/facebook-pixel'
import type { FathomAnalyticsOptions } from './registry/fathom-analytics'
import type { HotjarOptions } from './registry/hotjar'
import type { IntercomOptions } from './registry/intercom'
import type { SegmentInput } from './registry/segment'
import type { CloudflareWebAnalyticsInput } from './registry/cloudflare-web-analytics'
import type { FacebookPixelInput } from './registry/facebook-pixel'
import type { FathomAnalyticsInput } from './registry/fathom-analytics'
import type { HotjarInput } from './registry/hotjar'
import type { IntercomInput } from './registry/intercom'
import type { ConfettiInput } from './registry/confetti'

export type NuxtUseScriptOptions<T = any> = Omit<UseScriptOptions<T>, 'trigger'> & {
/**
Expand All @@ -32,6 +33,8 @@ export type NuxtUseScriptOptions<T = any> = Omit<UseScriptOptions<T>, 'trigger'>
beforeInit?: () => void
}

export type NuxtUseScriptIntegrationOptions = Omit<NuxtUseScriptOptions, 'use'>

export type NuxtUseScriptInput = UseScriptInput

export interface TrackedPage {
Expand Down Expand Up @@ -66,19 +69,18 @@ export interface NuxtAppScript {
}[]
}

export type ScriptRegistryEntry<T extends ObjectSchema<any>> = Input<T> | [Input<T>, NuxtUseScriptOptions<T>]
export type ScriptRegistryEntry<T> = T | [T, NuxtUseScriptOptions<T>]

export interface ScriptRegistry {
cloudflareWebAnalytics?: ScriptRegistryEntry<typeof CloudflareWebAnalyticsOptions>
confetti?: ScriptRegistryEntry<typeof CloudflareWebAnalyticsOptions>
facebookPixel?: ScriptRegistryEntry<typeof FacebookPixelOptions>
fathomAnalytics?: ScriptRegistryEntry<typeof FathomAnalyticsOptions>
hotjar?: ScriptRegistryEntry<typeof HotjarOptions>
segment?: ScriptRegistryEntry<typeof SegmentOptions>
intercom?: ScriptRegistryEntry<typeof IntercomOptions>
// TODO augment upstream (ga, gtm, etc)
cloudflareWebAnalytics?: ScriptRegistryEntry<CloudflareWebAnalyticsInput>
confetti?: ScriptRegistryEntry<ConfettiInput>
facebookPixel?: ScriptRegistryEntry<FacebookPixelInput>
fathomAnalytics?: ScriptRegistryEntry<FathomAnalyticsInput>
hotjar?: ScriptRegistryEntry<HotjarInput>
intercom?: ScriptRegistryEntry<IntercomInput>
segment?: ScriptRegistryEntry<SegmentInput>
}

export type ScriptDynamicSrcInput<T extends ObjectSchema<any>> = Input<T> & { src?: string }

export type RegistryScripts = (Import & { src?: string, key?: string, transform?: (options: any) => string })[]
export type RegistryScripts = (Import & { src?: string | false, module?: '@nuxt/scripts' | string, key?: string, transform?: (options: any) => string })[]

0 comments on commit c3db3e4

Please sign in to comment.