Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ export default defineNuxtConfig({
scripts: {
debug: true,
registry: {
googleSignIn: true,
blueskyEmbed: true,
xEmbed: true,
instagramEmbed: true,
googleMaps: true,
googleSignIn: {},
blueskyEmbed: {},
xEmbed: {},
instagramEmbed: {},
googleMaps: {},
},
},
})
2 changes: 1 addition & 1 deletion src/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
route: assetsBaseURL,
handler: lazyEventHandler(async () => {
return eventHandler(async (event) => {
const cleanPath = event.path.split('?')[0].slice(1)
const cleanPath = event.path!.split('?')[0].slice(1)

Check failure on line 56 in src/assets.ts

View workflow job for this annotation

GitHub Actions / test

Object is possibly 'undefined'.
const filename = cleanPath
const scriptDescriptor = renderedScript.get(join(assetsBaseURL, cleanPath))

Expand Down
3 changes: 2 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ export interface ModuleOptions {
*/
firstParty?: boolean | FirstPartyOptions
/**
* The registry of supported third-party scripts. Loads the scripts in globally using the default script options.
* The registry of supported third-party scripts. Presence enables infrastructure (proxy routes, types, bundling, composable auto-imports).
* Scripts only auto-load globally when `trigger` is explicitly set in the config object.
*/
registry?: NuxtConfigScriptRegistry
/**
Expand Down
38 changes: 29 additions & 9 deletions src/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
/**
* Normalize all registry config entries in-place to [input, scriptOptions?] tuple form.
* Eliminates the 4-shape polymorphism (true | 'mock' | object | [object, options])
* so all downstream consumers handle a single shape.
*
* - `true` → `[{}]`
* User-facing config shapes:
* - `false` → deleted
* - `'mock'` → `[{}, { trigger: 'manual', skipValidation: true }]`
* - `{ id: '...' }` → `[{ id: '...' }]`
* - `[{ id: '...' }, opts]` → unchanged
* - falsy / empty array → deleted
* - `{}` → `[{}]` (infrastructure only, no auto-load)
* - `{ id: '...', trigger: 'onNuxtReady' }` → `[{ id: '...' }, { trigger: 'onNuxtReady' }]`
* - `{ id: '...', trigger: 'onNuxtReady', scriptOptions: { warmupStrategy: 'preconnect' } }` → `[{ id: '...' }, { trigger: 'onNuxtReady', warmupStrategy: 'preconnect' }]`
* - `[input, scriptOptions]` → unchanged (internal/backwards compat)
*
* Removed:
* - `true` → build error with migration message
* - `'proxy-only'` → build error with migration message
*/
export function normalizeRegistryConfig(registry: Record<string, any>): void {
for (const key of Object.keys(registry)) {
Expand All @@ -17,9 +21,18 @@ export function normalizeRegistryConfig(registry: Record<string, any>): void {
continue
}
if (entry === true) {
registry[key] = [{}]
throw new Error(
`[nuxt-scripts] registry.${key}: boolean \`true\` is no longer supported. `
+ `Use \`{}\` for infrastructure only (composable/component driven) or \`{ trigger: 'onNuxtReady' }\` for global auto-loading.`,
)
}
if (entry === 'proxy-only') {
throw new Error(
`[nuxt-scripts] registry.${key}: \`'proxy-only'\` is no longer supported. `
+ `Use \`{}\` instead (infrastructure only is now the default behavior).`,
)
}
else if (entry === 'mock') {
if (entry === 'mock') {
registry[key] = [{}, { trigger: 'manual', skipValidation: true }]
}
else if (Array.isArray(entry)) {
Expand All @@ -31,7 +44,14 @@ export function normalizeRegistryConfig(registry: Record<string, any>): void {
entry[0] = {}
}
else if (typeof entry === 'object') {
registry[key] = [entry]
const { trigger, scriptOptions, ...input } = entry
const mergedScriptOptions = {
...(trigger !== undefined ? { trigger } : {}),
...scriptOptions,
}
registry[key] = Object.keys(mergedScriptOptions).length > 0
? [input, mergedScriptOptions]
: [input]
}
else {
delete registry[key]
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export interface NuxtDevToolsScriptInstance {
export interface ScriptRegistry {
bingUet?: BingUetInput
blueskyEmbed?: BlueskyEmbedInput
carbonAds?: true
carbonAds?: Record<string, never>
crisp?: CrispInput
clarity?: ClarityInput
cloudflareWebAnalytics?: CloudflareWebAnalyticsInput
Expand Down Expand Up @@ -226,7 +226,7 @@ export type BuiltInRegistryScriptKey
*/
export type RegistryScriptKey = Exclude<keyof ScriptRegistry, `${string}-npm`>

export type NuxtConfigScriptRegistryEntry<T> = true | false | 'mock' | T | [T, NuxtUseScriptOptionsSerializable]
export type NuxtConfigScriptRegistryEntry<T> = false | 'mock' | (T & { trigger?: NuxtUseScriptOptionsSerializable['trigger'], scriptOptions?: Omit<NuxtUseScriptOptionsSerializable, 'trigger'> }) | [T, NuxtUseScriptOptionsSerializable]
export type NuxtConfigScriptRegistry<T extends keyof ScriptRegistry = keyof ScriptRegistry> = Partial<{
[key in T]: NuxtConfigScriptRegistryEntry<ScriptRegistry[key]>
}> & Record<string & {}, NuxtConfigScriptRegistryEntry<any>>
Expand Down
44 changes: 22 additions & 22 deletions src/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,36 +117,36 @@ export function templatePlugin(config: Partial<ModuleOptions>, registry: Require

let needsServiceWorkerImport = false

// Registry entries are pre-normalized to [input, scriptOptions?] tuple form
// Registry entries are pre-normalized to [input, scriptOptions?] tuple form.
// Only generate a global composable call when scriptOptions.trigger is present;
// entries without a trigger are infrastructure only (proxy routes, types, bundling).
for (const [k, c] of Object.entries(config.registry || {})) {
if (c === false)
continue
const [, scriptOptions] = c as [Record<string, any>, any?]
if (!scriptOptions?.trigger)
continue
const importDefinition = registry.find(i => i.import.name.toLowerCase() === `usescript${k.toLowerCase()}`)
if (importDefinition) {
resolvedRegistryKeys.push(k)
imports.unshift(`import { ${importDefinition.import.name} } from '${importDefinition.import.from}'`)
const [input, scriptOptions] = c as [Record<string, any>, any?]
if (scriptOptions) {
const opts = { ...scriptOptions }
const triggerResolved = resolveTriggerForTemplate(opts.trigger)
if (triggerResolved) {
opts.trigger = '__TRIGGER_PLACEHOLDER__' as any
if (triggerResolved.includes('useScriptTriggerIdleTimeout'))
needsIdleTimeoutImport = true
if (triggerResolved.includes('useScriptTriggerInteraction'))
needsInteractionImport = true
if (triggerResolved.includes('useScriptTriggerServiceWorker'))
needsServiceWorkerImport = true
}
const args = { ...input, scriptOptions: opts }
const argsJson = triggerResolved
? JSON.stringify(args).replace(TRIGGER_PLACEHOLDER_RE, triggerResolved)
: JSON.stringify(args)
inits.push(`const ${k} = ${importDefinition.import.name}(${argsJson})`)
}
else {
inits.push(`const ${k} = ${importDefinition.import.name}(${JSON.stringify(input)})`)
const [input] = c as [Record<string, any>, any?]
const opts = { ...scriptOptions }
const triggerResolved = resolveTriggerForTemplate(opts.trigger)
if (triggerResolved) {
opts.trigger = '__TRIGGER_PLACEHOLDER__' as any
if (triggerResolved.includes('useScriptTriggerIdleTimeout'))
needsIdleTimeoutImport = true
if (triggerResolved.includes('useScriptTriggerInteraction'))
needsInteractionImport = true
if (triggerResolved.includes('useScriptTriggerServiceWorker'))
needsServiceWorkerImport = true
}
const args = { ...input, scriptOptions: opts }
const argsJson = triggerResolved
? JSON.stringify(args).replace(TRIGGER_PLACEHOLDER_RE, triggerResolved)
: JSON.stringify(args)
inits.push(`const ${k} = ${importDefinition.import.name}(${argsJson})`)
}
}
for (const [k, c] of Object.entries(config.globals || {})) {
Expand Down
8 changes: 4 additions & 4 deletions test/fixtures/basic/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ export default defineNuxtConfig({
],
scripts: {
registry: {
xEmbed: true,
instagramEmbed: true,
blueskyEmbed: true,
gravatar: true,
xEmbed: {},
instagramEmbed: {},
blueskyEmbed: {},
gravatar: {},
},
},
devtools: {
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/first-party/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default defineNuxtConfig({
posthog: [{ apiKey: 'phc_CkMaDU6dr11eJoQdAiSJb1rC324dogk3T952gJ6fD9W' }, manual],
intercom: [{ app_id: 'akg5rmxb' }, manual],
crisp: [{ id: 'b1021910-7ace-425a-9ef5-07f49e5ce417' }, manual],
vercelAnalytics: [true, manual],
vercelAnalytics: [{}, manual],
},
},
})
30 changes: 15 additions & 15 deletions test/unit/auto-inject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,56 +85,56 @@ describe('autoInject via proxy configs', () => {
})
})

describe('boolean entries', () => {
it('injects into runtimeConfig for posthog: true', () => {
const registry: any = { posthog: true }
describe('empty object entries (env var driven)', () => {
it('injects into runtimeConfig for posthog: {}', () => {
const registry: any = { posthog: {} }
const rt = makeRuntimeConfig({ posthog: { apiKey: '' } })

autoInjectAll(registry, rt, '/_proxy')

// After normalization, true becomes [{}] — both input and runtimeConfig get the value
// After normalization, {} becomes [{}] — both input and runtimeConfig get the value
expect(registry.posthog[0].apiHost).toBe('/_proxy/ph')
expect(rt.public.scripts.posthog.apiHost).toBe('/_proxy/ph')
})

it('uses EU prefix for posthog: true when runtime region is eu', () => {
const registry: any = { posthog: true }
it('uses EU prefix for posthog: {} when runtime region is eu', () => {
const registry: any = { posthog: {} }
const rt = makeRuntimeConfig({ posthog: { apiKey: '', region: 'eu' } })

autoInjectAll(registry, rt, '/_proxy')

expect(rt.public.scripts.posthog.apiHost).toBe('/_proxy/ph-eu')
})

it('injects into runtimeConfig for plausibleAnalytics: true', () => {
const registry: any = { plausibleAnalytics: true }
it('injects into runtimeConfig for plausibleAnalytics: {}', () => {
const registry: any = { plausibleAnalytics: {} }
const rt = makeRuntimeConfig({ plausibleAnalytics: { domain: '' } })

autoInjectAll(registry, rt, '/_proxy')

expect(rt.public.scripts.plausibleAnalytics.endpoint).toBe('/_proxy/plausible/api/event')
})

it('injects into runtimeConfig for umamiAnalytics: true', () => {
const registry: any = { umamiAnalytics: true }
it('injects into runtimeConfig for umamiAnalytics: {}', () => {
const registry: any = { umamiAnalytics: {} }
const rt = makeRuntimeConfig({ umamiAnalytics: { websiteId: '' } })

autoInjectAll(registry, rt, '/_proxy')

expect(rt.public.scripts.umamiAnalytics.hostUrl).toBe('/_proxy/umami')
})

it('injects into runtimeConfig for rybbitAnalytics: true', () => {
const registry: any = { rybbitAnalytics: true }
it('injects into runtimeConfig for rybbitAnalytics: {}', () => {
const registry: any = { rybbitAnalytics: {} }
const rt = makeRuntimeConfig({ rybbitAnalytics: { siteId: '' } })

autoInjectAll(registry, rt, '/_proxy')

expect(rt.public.scripts.rybbitAnalytics.analyticsHost).toBe('/_proxy/rybbit/api')
})

it('injects into runtimeConfig for databuddyAnalytics: true', () => {
const registry: any = { databuddyAnalytics: true }
it('injects into runtimeConfig for databuddyAnalytics: {}', () => {
const registry: any = { databuddyAnalytics: {} }
const rt = makeRuntimeConfig({ databuddyAnalytics: { clientId: '' } })

autoInjectAll(registry, rt, '/_proxy')
Expand Down Expand Up @@ -185,7 +185,7 @@ describe('autoInject via proxy configs', () => {

describe('custom proxyPrefix', () => {
it('uses custom prefix in computed values', () => {
const registry: any = { posthog: true }
const registry: any = { posthog: {} }
const rt = makeRuntimeConfig({ posthog: { apiKey: '' } })

autoInjectAll(registry, rt, '/_analytics')
Expand Down
47 changes: 42 additions & 5 deletions test/unit/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('template plugin file', () => {
`)
})
// registry
it('registry object', async () => {
it('registry object without trigger (infrastructure only, no composable call)', async () => {
const res = templatePluginNormalized({
globals: {},
registry: {
Expand All @@ -125,9 +125,27 @@ describe('template plugin file', () => {
},
},
])
expect(res).toContain('useScriptStripe({"id":"test"})')
expect(res).not.toContain('useScriptStripe')
})
it('registry array', async () => {
it('registry object with trigger (auto-loads globally)', async () => {
const res = templatePluginNormalized({
globals: {},
registry: {
stripe: {
id: 'test',
trigger: 'onNuxtReady',
},
},
}, [
{
import: {
name: 'useScriptStripe',
},
},
])
expect(res).toContain('useScriptStripe({"id":"test","scriptOptions":{"trigger":"onNuxtReady"}})')
})
it('registry array with trigger', async () => {
const res = templatePluginNormalized({
globals: {},
registry: {
Expand All @@ -150,7 +168,7 @@ describe('template plugin file', () => {
expect(res).toContain('useScriptStripe({"id":"test","scriptOptions":{"trigger":"onNuxtReady"}})')
})

it('registry with partytown option', async () => {
it('registry with partytown but no trigger (no composable call)', async () => {
const res = templatePluginNormalized({
globals: {},
registry: {
Expand All @@ -166,7 +184,26 @@ describe('template plugin file', () => {
},
},
])
expect(res).toContain('useScriptGoogleAnalytics({"id":"G-XXXXX","scriptOptions":{"partytown":true}})')
expect(res).not.toContain('useScriptGoogleAnalytics')
})

it('registry with partytown and trigger', async () => {
const res = templatePluginNormalized({
globals: {},
registry: {
googleAnalytics: [
{ id: 'G-XXXXX' },
{ partytown: true, trigger: 'onNuxtReady' },
],
},
}, [
{
import: {
name: 'useScriptGoogleAnalytics',
},
},
])
expect(res).toContain('useScriptGoogleAnalytics({"id":"G-XXXXX","scriptOptions":{"partytown":true,"trigger":"onNuxtReady"}})')
})

// Test idleTimeout trigger in globals
Expand Down
Loading