Skip to content

Commit 4b94b5b

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 7f21bc3 + a647d12 commit 4b94b5b

File tree

11 files changed

+299
-494
lines changed

11 files changed

+299
-494
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
"lint": "eslint .",
4949
"lint:fix": "eslint . --fix",
5050
"test": "vitest",
51-
"test:types": "npx nuxi typecheck"
51+
"test:types": "npx nuxi typecheck",
52+
"script:generate-tpc": "bun ./scripts/generateTpcScripts.ts && pnpm lint:fix"
5253
},
5354
"build": {
5455
"externals": [

scripts/generateTpcScripts.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { writeFile } from 'node:fs/promises'
2+
import { GooglaAnalyticsData, GoogleTagManagerData, type Output } from 'third-party-capital'
3+
import type { UseScriptInput } from '@unhead/vue'
4+
import { resolve } from 'pathe'
5+
import type { NuxtUseScriptOptions } from '../src/runtime/types'
6+
import { registry } from '../src/registry'
7+
import { generateTpcContent } from './utils'
8+
9+
export interface TpcDescriptor {
10+
label: string
11+
tpcKey: string
12+
tpcData: Output
13+
tpcTypeImport: string
14+
key: string
15+
registry?: any
16+
options: ({
17+
scriptInput?: UseScriptInput
18+
scriptOptions?: NuxtUseScriptOptions
19+
schema?: any
20+
clientInit?: () => void
21+
})
22+
}
23+
24+
const scripts: TpcDescriptor[] = [
25+
// GTM
26+
{
27+
label: 'Google Tag Manager',
28+
tpcKey: 'gtm',
29+
tpcData: GoogleTagManagerData as Output,
30+
tpcTypeImport: 'GoogleTagManagerApi',
31+
key: 'google-tag-manager',
32+
options: {
33+
scriptOptions: {
34+
performanceMarkFeature: 'nuxt-third-parties-gtm',
35+
use: () => {
36+
// @ts-expect-error untyped
37+
return { dataLayer: window.dataLayer, google_tag_manager: window.google_tag_manager }
38+
},
39+
stub: ({ fn }) => {
40+
return fn === 'dataLayer' ? [] : undefined
41+
},
42+
},
43+
},
44+
},
45+
// GA
46+
{
47+
label: 'Google Analytics',
48+
tpcKey: 'gtag',
49+
tpcData: GooglaAnalyticsData as Output,
50+
key: 'google-analytics',
51+
tpcTypeImport: 'GoogleAnalyticsApi',
52+
options: {
53+
scriptOptions: {
54+
performanceMarkFeature: 'nuxt-third-parties-ga',
55+
use: () => {
56+
// @ts-expect-error untyped
57+
return { dataLayer: window.dataLayer, gtag: window.gtag }
58+
},
59+
// allow dataLayer to be accessed on the server
60+
stub: ({ fn }) => {
61+
return fn === 'dataLayer' ? [] : undefined
62+
},
63+
},
64+
},
65+
}]
66+
67+
export async function generate() {
68+
for (const script of scripts) {
69+
script.registry = registry().find(r => r.label === script.label)
70+
const content = await generateTpcContent(script)
71+
await writeFile(resolve(`./src/runtime/registry/${script.key}.ts`), content)
72+
}
73+
}
74+
75+
generate()

scripts/utils.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { ExternalScript, Script } from 'third-party-capital'
2+
import { genImport } from 'knitwork'
3+
import type { HeadEntryOptions } from '@unhead/vue'
4+
import type { TpcDescriptor } from './generateTpcScripts'
5+
6+
const HEAD_VAR = '__head'
7+
const INJECTHEAD_CODE = `const ${HEAD_VAR} = injectHead()`
8+
9+
export async function generateTpcContent(input: TpcDescriptor) {
10+
if (!input.tpcData.scripts)
11+
throw new Error('input.data has no scripts !')
12+
13+
// replace all empty spaces with nothing
14+
const titleKey = input.label.replace(/\s/g, '')
15+
16+
const mainScript = input.tpcData.scripts?.find(({ key }) => key === input.tpcKey) as ExternalScript
17+
18+
if (!mainScript)
19+
throw new Error(`no main script found for ${input.tpcKey} in third-party-capital`)
20+
21+
const mainScriptOptions = getScriptInputOption(mainScript)
22+
23+
const imports = new Set<string>([
24+
'import { withQuery } from \'ufo\'',
25+
'import { useRegistryScript } from \'#nuxt-scripts-utils\'',
26+
'import type { RegistryScriptInput } from \'#nuxt-scripts\'',
27+
`import type { ${input.tpcTypeImport} } from 'third-party-capital'`,
28+
])
29+
30+
const chunks: string[] = []
31+
const functionBody: string[] = []
32+
33+
const hasParams = mainScript.params?.length
34+
35+
if (hasParams) {
36+
imports.add(genImport('#nuxt-scripts-validator', ['object', 'string']))
37+
// need schema validation from tpc
38+
chunks.push(`export const ${titleKey}Options = object({${mainScript.params?.map(p => `${p}: string()`)}})`)
39+
}
40+
41+
chunks.push(`
42+
declare global {
43+
interface Window extends ${input.tpcTypeImport} {}
44+
}`)
45+
46+
const clientInitCode: string[] = []
47+
48+
if (input.tpcData.stylesheets) {
49+
if (!functionBody.includes(INJECTHEAD_CODE)) {
50+
functionBody.unshift(INJECTHEAD_CODE)
51+
}
52+
functionBody.push(`${HEAD_VAR}.push({link: ${JSON.stringify(input.tpcData.stylesheets.map(s => ({ ref: 'stylesheet', href: s })))}})`)
53+
}
54+
55+
for (const script of input.tpcData.scripts) {
56+
if ('code' in script)
57+
clientInitCode.push(replaceTokenToRuntime(script.code))
58+
59+
if (script === mainScript)
60+
continue
61+
62+
if ('url' in script) {
63+
functionBody.push(`${HEAD_VAR}.push({scripts:{ async: true, src: ${script.url} }},${JSON.stringify(getScriptInputOption(script))})`)
64+
}
65+
}
66+
67+
chunks.push(`export type ${titleKey}Input = RegistryScriptInput${hasParams ? `<typeof ${titleKey}Options>` : ''}`)
68+
69+
chunks.push(`
70+
export function ${input.registry.import!.name}<T extends ${input.tpcTypeImport}>(_options?: ${titleKey}Input) {
71+
${functionBody.join('\n')}
72+
return useRegistryScript${hasParams ? `<T, typeof ${titleKey}Options>` : ''}(_options?.key || '${input.key}', options => ({
73+
scriptInput: {
74+
src: withQuery('${mainScript.url}', {${mainScript.params?.map(p => `${p}: options?.${p}`)}})
75+
},
76+
schema: import.meta.dev ? undefined : ${titleKey}Options,
77+
scriptOptions: {
78+
use: ${input.options.scriptOptions!.use!.toString()},
79+
stub: import.meta.client ? undefined : ${input.options.scriptOptions!.stub!.toString()},
80+
${input.options.scriptOptions?.performanceMarkFeature ? `performanceMarkFeature: ${JSON.stringify(input.options.scriptOptions?.performanceMarkFeature)},` : ''}
81+
${mainScriptOptions ? `...(${JSON.stringify(mainScriptOptions)})` : ''}
82+
},
83+
// eslint-disable-next-line
84+
${clientInitCode.length ? `clientInit: import.meta.server ? undefined : () => {${clientInitCode.join('\n')}},` : ''}
85+
}), _options)
86+
}`)
87+
88+
chunks.unshift(...Array.from(imports))
89+
chunks.unshift('// WARNING: This file is automatically generated, do not manually modify.')
90+
return chunks.join('\n')
91+
}
92+
93+
function replaceTokenToRuntime(code: string) {
94+
return code.split(';').map(c => c.replaceAll(/'?\{\{(.*?)\}\}'?/g, 'options.$1')).join(';')
95+
}
96+
97+
function getScriptInputOption(script: Script): HeadEntryOptions | undefined {
98+
if (script.location === 'body') {
99+
if (script.action === 'append') {
100+
return { tagPosition: 'bodyClose' }
101+
}
102+
return { tagPosition: 'bodyOpen' }
103+
}
104+
105+
if (script.action === 'append') {
106+
return { tagPriority: 1 }
107+
}
108+
}

src/module.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import type {
2727
} from './runtime/types'
2828
import { NuxtScriptsCheckScripts } from './plugins/check-scripts'
2929
import { templatePlugin } from './templates'
30-
import { addTpc } from './tpc/addTpc'
3130

3231
export interface ModuleOptions {
3332
/**
@@ -138,7 +137,6 @@ export default defineNuxtModule<ModuleOptions>({
138137
})
139138

140139
const scripts = registry(resolve)
141-
addTpc(scripts)
142140
nuxt.hooks.hook('modules:done', async () => {
143141
const registryScripts = [...scripts]
144142

src/registry.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { joinURL, withBase, withQuery } from 'ufo'
2+
import {
3+
GoogleAnalytics,
4+
GoogleTagManager,
5+
} from 'third-party-capital'
26
import type { HotjarInput } from './runtime/registry/hotjar'
37
import type { IntercomInput } from './runtime/registry/intercom'
48
import type { SegmentInput } from './runtime/registry/segment'
@@ -228,5 +232,41 @@ export const registry: (resolve?: (s: string) => string) => RegistryScripts = (r
228232
from: resolve('./runtime/registry/npm'),
229233
},
230234
},
235+
{
236+
label: 'Google Tag Manager',
237+
category: 'tracking',
238+
import: {
239+
name: 'useScriptGoogleTagManager',
240+
from: resolve('./runtime/registry/google-tag-manager'),
241+
},
242+
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="#8AB4F8" d="m150.262 245.516l-44.437-43.331l95.433-97.454l46.007 45.091z"/><path fill="#4285F4" d="M150.45 53.938L106.176 8.731L9.36 104.629c-12.48 12.48-12.48 32.713 0 45.207l95.36 95.986l45.09-42.182l-72.654-76.407z"/><path fill="#8AB4F8" d="m246.625 105.37l-96-96c-12.494-12.494-32.756-12.494-45.25 0c-12.495 12.495-12.495 32.757 0 45.252l96 96c12.494 12.494 32.756 12.494 45.25 0c12.495-12.495 12.495-32.757 0-45.251"/><circle cx="127.265" cy="224.731" r="31.273" fill="#246FDB"/></svg>`,
243+
scriptBundling(options) {
244+
const data = GoogleTagManager(options)
245+
const mainScript = data.scripts?.find(({ key }) => key === 'gtag')
246+
247+
if (mainScript && 'url' in mainScript && mainScript.url)
248+
return mainScript.url
249+
250+
return false
251+
},
252+
},
253+
{
254+
label: 'Google Analytics',
255+
category: 'tracking',
256+
import: {
257+
name: 'useScriptGoogleAnalytics',
258+
from: resolve('./runtime/registry/google-analytics'),
259+
},
260+
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="#8AB4F8" d="m150.262 245.516l-44.437-43.331l95.433-97.454l46.007 45.091z"/><path fill="#4285F4" d="M150.45 53.938L106.176 8.731L9.36 104.629c-12.48 12.48-12.48 32.713 0 45.207l95.36 95.986l45.09-42.182l-72.654-76.407z"/><path fill="#8AB4F8" d="m246.625 105.37l-96-96c-12.494-12.494-32.756-12.494-45.25 0c-12.495 12.495-12.495 32.757 0 45.252l96 96c12.494 12.494 32.756 12.494 45.25 0c12.495-12.495 12.495-32.757 0-45.251"/><circle cx="127.265" cy="224.731" r="31.273" fill="#246FDB"/></svg>`,
261+
scriptBundling(options) {
262+
const data = GoogleAnalytics(options)
263+
const mainScript = data.scripts?.find(({ key }) => key === 'gtag')
264+
265+
if (mainScript && 'url' in mainScript && mainScript.url)
266+
return mainScript.url
267+
268+
return false
269+
},
270+
},
231271
]
232272
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// WARNING: This file is automatically generated, do not manually modify.
2+
import { withQuery } from 'ufo'
3+
import type { GoogleAnalyticsApi } from 'third-party-capital'
4+
import { useRegistryScript } from '#nuxt-scripts-utils'
5+
import type { RegistryScriptInput } from '#nuxt-scripts'
6+
import { object, string } from '#nuxt-scripts-validator'
7+
8+
export const GoogleAnalyticsOptions = object({ id: string() })
9+
10+
declare global {
11+
interface Window extends GoogleAnalyticsApi {}
12+
}
13+
export type GoogleAnalyticsInput = RegistryScriptInput<typeof GoogleAnalyticsOptions>
14+
15+
export function useScriptGoogleAnalytics<T extends GoogleAnalyticsApi>(_options?: GoogleAnalyticsInput) {
16+
return useRegistryScript<T, typeof GoogleAnalyticsOptions>(_options?.key || 'google-analytics', options => ({
17+
scriptInput: {
18+
src: withQuery('https://www.googletagmanager.com/gtag/js', { id: options?.id }),
19+
},
20+
schema: import.meta.dev ? undefined : GoogleAnalyticsOptions,
21+
scriptOptions: {
22+
use: () => {
23+
return { dataLayer: window.dataLayer, gtag: window.gtag }
24+
},
25+
stub: import.meta.client
26+
? undefined
27+
: ({ fn }) => {
28+
return fn === 'dataLayer' ? [] : void 0
29+
},
30+
performanceMarkFeature: 'nuxt-third-parties-ga',
31+
...({ tagPriority: 1 }),
32+
},
33+
// eslint-disable-next-line
34+
clientInit: import.meta.server ? undefined : () => {window.dataLayer=window.dataLayer||[];window.gtag=function gtag(){window.dataLayer.push(arguments);};window.gtag('js',new Date());window.gtag('config',options.id)},
35+
}), _options)
36+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// WARNING: This file is automatically generated, do not manually modify.
2+
import { withQuery } from 'ufo'
3+
import type { GoogleTagManagerApi } from 'third-party-capital'
4+
import { useRegistryScript } from '#nuxt-scripts-utils'
5+
import type { RegistryScriptInput } from '#nuxt-scripts'
6+
import { object, string } from '#nuxt-scripts-validator'
7+
8+
export const GoogleTagManagerOptions = object({ id: string() })
9+
10+
declare global {
11+
interface Window extends GoogleTagManagerApi {}
12+
}
13+
export type GoogleTagManagerInput = RegistryScriptInput<typeof GoogleTagManagerOptions>
14+
15+
export function useScriptGoogleTagManager<T extends GoogleTagManagerApi>(_options?: GoogleTagManagerInput) {
16+
return useRegistryScript<T, typeof GoogleTagManagerOptions>(_options?.key || 'google-tag-manager', options => ({
17+
scriptInput: {
18+
src: withQuery('https://www.googletagmanager.com/gtm.js', { id: options?.id }),
19+
},
20+
schema: import.meta.dev ? undefined : GoogleTagManagerOptions,
21+
scriptOptions: {
22+
use: () => {
23+
return { dataLayer: window.dataLayer, google_tag_manager: window.google_tag_manager }
24+
},
25+
stub: import.meta.client
26+
? undefined
27+
: ({ fn }) => {
28+
return fn === 'dataLayer' ? [] : void 0
29+
},
30+
performanceMarkFeature: 'nuxt-third-parties-gtm',
31+
...({ tagPriority: 1 }),
32+
},
33+
// eslint-disable-next-line
34+
clientInit: import.meta.server ? undefined : () => {window.dataLayer=window.dataLayer||[];window.dataLayer.push({'gtm.start':new Date().getTime(),event:'gtm.js'});},
35+
}), _options)
36+
}

src/runtime/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import type { LemonSqueezyInput } from './registry/lemon-squeezy'
2222
import type { GoogleAdsenseInput } from './registry/google-adsense'
2323
import type { ClarityInput } from './registry/clarity'
2424
import type { CrispInput } from './registry/crisp'
25-
import type { Input as GoogleTagManagerInput } from '#build/modules/nuxt-scripts-gtm'
26-
import type { Input as GoogleAnalyticsInput } from '#build/modules/nuxt-scripts-ga'
25+
import type { GoogleAnalyticsInput } from './registry/google-analytics'
26+
import type { GoogleTagManagerInput } from './registry/google-tag-manager'
2727

2828
export type NuxtUseScriptOptions<T = any> = Omit<UseScriptOptions<T>, 'trigger'> & {
2929
/**

0 commit comments

Comments
 (0)