Skip to content

Commit

Permalink
fix(tpc): support customizing dataLayer variable
Browse files Browse the repository at this point in the history
  • Loading branch information
huang-julien authored Jul 30, 2024
1 parent 3c8e6c4 commit 09e3c2f
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 61 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"shiki": "^1.10.3",
"sirv": "^2.0.4",
"std-env": "^3.7.0",
"third-party-capital": "^1.1.1",
"third-party-capital": "^2.1.1",
"ufo": "^1.5.3",
"unimport": "^3.7.2",
"unplugin": "^1.11.0",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 8 additions & 12 deletions scripts/generateTpcScripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ export interface TpcDescriptor {
label: string
tpcKey: string
tpcData: Output
tpcTypeImport: string
tpcTypeAugmentation?: string
tpcTypesImport?: string[]
key: string
registry?: any
scriptInput?: UseScriptInput
performanceMarkFeature?: string
returnUse?: string
useBody?: string
returnStub?: string
clientInit?: string
defaultOptions?: Record<string, unknown>
Expand All @@ -28,14 +29,12 @@ const scripts: Array<TpcDescriptor> = [
label: 'Google Tag Manager',
tpcKey: 'gtm',
tpcData: GoogleTagManagerData as Output,
tpcTypeImport: 'GoogleTagManagerApi',
tpcTypeAugmentation: 'GoogleTagManagerApi',
tpcTypesImport: ['DataLayer'],
key: 'googleTagManager',
performanceMarkFeature: 'nuxt-third-parties-gtm',
returnUse: '{ dataLayer: window.dataLayers[options.dataLayerName!], google_tag_manager: window.google_tag_manager }',
useBody: 'return { dataLayer: (window as any)[options.l ?? "dataLayer"] as DataLayer, google_tag_manager: window.google_tag_manager }',
returnStub: 'fn === \'dataLayer\' ? [] : void 0',
defaultOptions: {
dataLayerName: 'defaultGtm',
},
},
// GA
{
Expand All @@ -44,14 +43,11 @@ const scripts: Array<TpcDescriptor> = [
tpcKey: 'gtag',
tpcData: GooglaAnalyticsData as Output,
key: 'googleAnalytics',
tpcTypeImport: 'GoogleAnalyticsApi',
tpcTypesImport: ['DataLayer', 'GTag'],
performanceMarkFeature: 'nuxt-third-parties-ga',
returnUse: '{ dataLayer: window.dataLayers[options.dataLayerName!], gtag: window.gtag }',
useBody: 'const gtag: GTag = function (...args:Parameters<GTag>) { \n((window as any)[options.l ?? "dataLayer"] as DataLayer).push(args);} as GTag\nreturn { dataLayer: (window as any)[options.l ?? "dataLayer"] as DataLayer,\n gtag }',
// allow dataLayer to be accessed on the server
returnStub: 'fn === \'dataLayer\' ? [] : void 0',
defaultOptions: {
dataLayerName: 'defaultGa',
},
}]

export async function generate() {
Expand Down
72 changes: 51 additions & 21 deletions scripts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,53 @@ export async function generateTpcContent(input: TpcDescriptor) {
'import { withQuery } from \'ufo\'',
'import { useRegistryScript } from \'#nuxt-scripts-utils\'',
'import type { RegistryScriptInput } from \'#nuxt-scripts\'',
`import type { ${input.tpcTypeImport} } from 'third-party-capital'`,
])
const tpcTypes = new Set<string>()

const chunks: string[] = []
const functionBody: string[] = []

if (input.tpcTypeAugmentation) {
tpcTypes.add(input.tpcTypeAugmentation)

chunks.push(`
declare global {
interface Window extends ${input.tpcTypeAugmentation} {}
}`)
}

if (input.tpcTypesImport) {
for (const typeImport of input.tpcTypesImport) {
tpcTypes.add(typeImport)
}
}

if (tpcTypes.size) {
imports.add(genImport('third-party-capital', [...tpcTypes]))
}

if (input.defaultOptions) {
imports.add(genImport('defu', ['defu']))
functionBody.push(`_options = defu(_options, ${JSON.stringify(input.defaultOptions)})`)
}

const params = [...new Set(input.tpcData.scripts?.map(s => s.params || []).flat() || [])]
const optionalParams = [...new Set(input.tpcData.scripts?.map(s => Object.keys(s.optionalParams) || []).flat() || [])]

if (params.length) {
if (params.length || optionalParams.length) {
const validatorImports = new Set<string>(['object', 'string'])
if (optionalParams.length) {
validatorImports.add('optional')
}

const properties = params.filter(p => !optionalParams.includes(p)).map(p => `${p}: string()`).concat(optionalParams.map(o => `${o}: optional(string())`))
// need schema validation from tpc
chunks.push(`export const ${titleKey}Options = object({${params.map((p) => {
if (input.defaultOptions && p in input.defaultOptions) {
validatorImports.add('optional')
return `${p}: optional(string())`
}
return `${p}: string()`
})}})`)
chunks.push(`export const ${titleKey}Options = object({
${properties.join(',\n')}
})`)
imports.add(genImport('#nuxt-scripts-validator', [...validatorImports]))
}

chunks.push(`
declare global {
interface Window extends ${input.tpcTypeImport} {}
}`)

const clientInitCode: string[] = []

if (input.tpcData.stylesheets) {
Expand All @@ -66,7 +82,7 @@ declare global {

for (const script of input.tpcData.scripts) {
if ('code' in script)
clientInitCode.push(replaceTokenToRuntime(script.code))
clientInitCode.push(replaceTokenToRuntime(script.code, script.optionalParams))

if (script === mainScript)
continue
Expand All @@ -78,21 +94,33 @@ declare global {

chunks.push(`export type ${titleKey}Input = RegistryScriptInput${params.length ? `<typeof ${titleKey}Options>` : ''}`)

if (input.useBody) {
chunks.push(`
function use(options: ${titleKey}Input) {
${input.useBody}
}
`)
}

const srcQueries = [...new Set<string>([...mainScript.params, ...Object.keys(mainScript.optionalParams)])].map(p => `${p}: options?.${p}`)

chunks.push(`
export function ${input.registry.import!.name}<T extends ${input.tpcTypeImport}>(_options?: ${titleKey}Input) {
export function ${input.registry.import!.name}(_options?: ${titleKey}Input) {
${functionBody.join('\n')}
return useRegistryScript${params.length ? `<T, typeof ${titleKey}Options>` : ''}(_options?.key || '${input.key}', options => ({
return useRegistryScript<${input.useBody ? `ReturnType<typeof use>` : `Record<string | symbol, any>`},${params.length ? `typeof ${titleKey}Options` : ''}>(_options?.key || '${input.key}', options => ({
scriptInput: {
src: withQuery('${mainScript.url}', {${mainScript.params?.map(p => `${p}: options?.${p}`)}})
src: withQuery('${mainScript.url}', {${srcQueries.join(', ')}}),
},
schema: import.meta.dev ? ${titleKey}Options : undefined,
scriptOptions: {
use: () => { return ${input.returnUse} },
${input.useBody ? `use: () => use(options),` : ''}
stub: import.meta.client ? undefined : ({fn}) => { return ${input.returnStub}},
${input.performanceMarkFeature ? `performanceMarkFeature: ${JSON.stringify(input.performanceMarkFeature)},` : ''}
${mainScriptOptions ? `...(${JSON.stringify(mainScriptOptions)})` : ''}
},
// eslint-disable-next-line
// @ts-ignore
// eslint-disable-next-line
${clientInitCode.length ? `clientInit: import.meta.server ? undefined : () => {${clientInitCode.join('\n')}},` : ''}
}), _options)
}`)
Expand All @@ -102,8 +130,10 @@ ${functionBody.join('\n')}
return chunks.join('\n')
}

function replaceTokenToRuntime(code: string) {
return code.split(';').map(c => c.replaceAll(/'?\{\{(.*?)\}\}'?/g, 'options.$1!')).join(';')
function replaceTokenToRuntime(code: string, defaultValues?: Record<string, string | number | undefined>) {
return code.split(';').map(c => c.replaceAll(/'?\{\{(.*?)\}\}'?/g, (_, token) => {
return `options?.${token} ${defaultValues?.[token] ? `?? ${JSON.stringify(defaultValues?.[token])}` : ''}`
})).join(';')
}

function getScriptInputOption(script: Script): HeadEntryOptions | undefined {
Expand Down
31 changes: 19 additions & 12 deletions src/runtime/registry/google-analytics.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
// WARNING: This file is automatically generated, do not manually modify.
import { withQuery } from 'ufo'
import type { GoogleAnalyticsApi } from 'third-party-capital'
import { defu } from 'defu'
import type { DataLayer, GTag } from 'third-party-capital'
import { useRegistryScript } from '#nuxt-scripts-utils'
import type { RegistryScriptInput } from '#nuxt-scripts'
import { object, string, optional } from '#nuxt-scripts-validator'

export const GoogleAnalyticsOptions = object({ id: string(), dataLayerName: optional(string()) })
export const GoogleAnalyticsOptions = object({
id: string(),
l: optional(string()),
})
export type GoogleAnalyticsInput = RegistryScriptInput<typeof GoogleAnalyticsOptions>

declare global {
interface Window extends GoogleAnalyticsApi {}
function use(options: GoogleAnalyticsInput) {
const gtag: GTag = function (...args: Parameters<GTag>) {
((window as any)[options.l ?? 'dataLayer'] as DataLayer).push(args)
} as GTag
return { dataLayer: (window as any)[options.l ?? 'dataLayer'] as DataLayer,
gtag }
}
export type GoogleAnalyticsInput = RegistryScriptInput<typeof GoogleAnalyticsOptions>

export function useScriptGoogleAnalytics<T extends GoogleAnalyticsApi>(_options?: GoogleAnalyticsInput) {
_options = defu(_options, { dataLayerName: 'defaultGa' })
return useRegistryScript<T, typeof GoogleAnalyticsOptions>(_options?.key || 'googleAnalytics', options => ({
export function useScriptGoogleAnalytics(_options?: GoogleAnalyticsInput) {
return useRegistryScript<ReturnType<typeof use>, typeof GoogleAnalyticsOptions>(_options?.key || 'googleAnalytics', options => ({
scriptInput: {
src: withQuery('https://www.googletagmanager.com/gtag/js', { id: options?.id }),
src: withQuery('https://www.googletagmanager.com/gtag/js', { id: options?.id, l: options?.l }),
},
schema: import.meta.dev ? GoogleAnalyticsOptions : undefined,
scriptOptions: {
use: () => { return { dataLayer: window.dataLayers[options.dataLayerName!], gtag: window.gtag } },
use: () => use(options),
stub: import.meta.client ? undefined : ({ fn }) => { return fn === 'dataLayer' ? [] : void 0 },
performanceMarkFeature: 'nuxt-third-parties-ga',
...({ tagPriority: 1 }),
},
// eslint-disable-next-line
clientInit: import.meta.server ? undefined : () => {window.dataLayers=window.dataLayers||{};window.dataLayers[options.dataLayerName!]=window.dataLayers[options.dataLayerName!]||[];window.gtag=function gtag(){window.dataLayer.push(arguments);};window.gtag('js',new Date());window.gtag('config',options.id!)},
// @ts-ignore
// eslint-disable-next-line
clientInit: import.meta.server ? undefined : () => {window[options?.l ?? "dataLayer"]=window[options?.l ?? "dataLayer"]||[];window[options?.l ?? "dataLayer"].push({'js':new Date()});window[options?.l ?? "dataLayer"].push({'config':options?.id })},
}), _options)
}
26 changes: 16 additions & 10 deletions src/runtime/registry/google-tag-manager.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
// WARNING: This file is automatically generated, do not manually modify.
import { withQuery } from 'ufo'
import type { GoogleTagManagerApi } from 'third-party-capital'
import { defu } from 'defu'
import type { GoogleTagManagerApi, DataLayer } from 'third-party-capital'
import { useRegistryScript } from '#nuxt-scripts-utils'
import type { RegistryScriptInput } from '#nuxt-scripts'
import { object, string, optional } from '#nuxt-scripts-validator'

export const GoogleTagManagerOptions = object({ id: string(), dataLayerName: optional(string()) })

declare global {
interface Window extends GoogleTagManagerApi {}
}
export const GoogleTagManagerOptions = object({
id: string(),
l: optional(string()),
})
export type GoogleTagManagerInput = RegistryScriptInput<typeof GoogleTagManagerOptions>

export function useScriptGoogleTagManager<T extends GoogleTagManagerApi>(_options?: GoogleTagManagerInput) {
_options = defu(_options, { dataLayerName: 'defaultGtm' })
return useRegistryScript<T, typeof GoogleTagManagerOptions>(_options?.key || 'googleTagManager', options => ({
function use(options: GoogleTagManagerInput) {
return { dataLayer: (window as any)[options.l ?? 'dataLayer'] as DataLayer, google_tag_manager: window.google_tag_manager }
}

export function useScriptGoogleTagManager(_options?: GoogleTagManagerInput) {
return useRegistryScript<ReturnType<typeof use>, typeof GoogleTagManagerOptions>(_options?.key || 'googleTagManager', options => ({
scriptInput: {
src: withQuery('https://www.googletagmanager.com/gtm.js', { id: options?.id }),
src: withQuery('https://www.googletagmanager.com/gtm.js', { id: options?.id, l: options?.l }),
},
schema: import.meta.dev ? GoogleTagManagerOptions : undefined,
scriptOptions: {
use: () => { return { dataLayer: window.dataLayers[options.dataLayerName!], google_tag_manager: window.google_tag_manager } },
use: () => use(options),
stub: import.meta.client ? undefined : ({ fn }) => { return fn === 'dataLayer' ? [] : void 0 },
performanceMarkFeature: 'nuxt-third-parties-gtm',
...({ tagPriority: 1 }),
},
// eslint-disable-next-line
clientInit: import.meta.server ? undefined : () => {window.dataLayers=window.dataLayers||{};window.dataLayers[options.dataLayerName!]=window.dataLayers[options.dataLayerName!]||[];window.dataLayers[options.dataLayerName!].push({'gtm.start':new Date().getTime(),event:'gtm.js'});},
// @ts-ignore
// eslint-disable-next-line
clientInit: import.meta.server ? undefined : () => {window[options?.l ?? "dataLayer"]=window[options?.l ?? "dataLayer"]||[];window[options?.l ?? "dataLayer"].push({'gtm.start':new Date().getTime(),event:'gtm.js'});},
}), _options)
}

0 comments on commit 09e3c2f

Please sign in to comment.