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
6 changes: 3 additions & 3 deletions src/runtime/composables/useScript.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { UseScriptInput, UseScriptOptions, VueScriptInstance } from '@unhead/vue/scripts'
import type { NuxtDevToolsScriptInstance, NuxtUseScriptOptions, UseFunctionType, UseScriptContext } from '../types'
import type { UseScriptOptions, VueScriptInstance } from '@unhead/vue/scripts'
import type { NuxtDevToolsScriptInstance, NuxtUseScriptInput, NuxtUseScriptOptions, UseFunctionType, UseScriptContext } from '../types'
// @ts-expect-error virtual template
import { resolveTrigger } from '#build/nuxt-scripts-trigger-resolver'
import { useScript as _useScript } from '@unhead/vue/scripts'
Expand Down Expand Up @@ -32,7 +32,7 @@ export function resolveScriptKey(input: any): string {
return input.key || input.src || (typeof input.innerHTML === 'string' ? input.innerHTML : '')
}

export function useScript<T extends Record<symbol | string, any> = Record<symbol | string, any>>(input: UseScriptInput, options?: NuxtUseScriptOptions<T>): UseScriptContext<UseFunctionType<NuxtUseScriptOptions<T>, T>> {
export function useScript<T extends Record<symbol | string, any> = Record<symbol | string, any>>(input: NuxtUseScriptInput, options?: NuxtUseScriptOptions<T>): UseScriptContext<UseFunctionType<NuxtUseScriptOptions<T>, T>> {
input = typeof input === 'string' ? { src: input } : input
Comment on lines +35 to 36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Forward custom attrs through the Partytown path.

After widening input, the options.partytown branch still emits only { src, type: 'text/partytown' }, so arbitrary attrs like tw-client-keyβ€”and existing fields like nonceβ€”are silently dropped there. That leaves this fix incomplete for Partytown users.

Suggested change
-    useHead({
-      script: [{ src, type: 'text/partytown' }],
-    })
+    useHead({
+      script: [{ ...input, src, type: 'text/partytown' }],
+    })
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/composables/useScript.ts` around lines 35 - 36, The partytown
branch in useScript currently replaces the script attrs with a new object
literal ({ src, type: 'text/partytown' }) and drops arbitrary/custom fields
(e.g., tw-client-key, nonce); update that branch inside useScript to merge and
forward all existing attributes by combining the original input and any
options.partytown into the emitted object (preserve input, include src and type:
'text/partytown' while spreading input and options.partytown so custom attrs and
nonce are retained). Ensure you reference the existing variables input and
options.partytown and produce the final object by merging (input,
options.partytown) with src/type enforced for Partytown.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the current NuxtUseScriptInput definition
fd -t f "types.ts" src/runtime/ --exec cat -n {}

Repository: nuxt/scripts

Length of output: 12564


🏁 Script executed:

# Check useScript.ts imports and the specific lines mentioned (35-36)
cat -n src/runtime/composables/useScript.ts | head -50

Repository: nuxt/scripts

Length of output: 2632


🏁 Script executed:

# Find the git diff to see what changed in these files
git diff HEAD~1 src/runtime/types.ts src/runtime/composables/useScript.ts

Repository: nuxt/scripts

Length of output: 244


🏁 Script executed:

# Search for useScript calls with string literals in test/example files
rg "useScript\s*\(\s*['\"]" --type ts --type js -A 2

Repository: nuxt/scripts

Length of output: 7698


🏁 Script executed:

# Also check if there are any imports of UseScriptInput to understand its structure
rg "UseScriptInput" --type ts

Repository: nuxt/scripts

Length of output: 1682


🏁 Script executed:

# Let's see what the `@unhead/vue/scripts` exports
# First, check if there are any node_modules or type definitions in the repo
fd -t f "node_modules/@unhead" -o -t f ".d.ts" | grep -i unhead | head -20

Repository: nuxt/scripts

Length of output: 172


🏁 Script executed:

# Look for any documentation or README that might mention useScript('url')
rg "useScript\s*\(\s*['\"]https" --type ts --type js --type md

Repository: nuxt/scripts

Length of output: 4435


🏁 Script executed:

# Check package.json to see the `@unhead/vue` version
cat package.json | grep -A 5 '"@unhead/vue"'

Repository: nuxt/scripts

Length of output: 374


🏁 Script executed:

# Search for any local type stubs or documentation about UseScriptInput
rg "UseScriptInput.*=" --type ts -B 2 -A 5

Repository: nuxt/scripts

Length of output: 1237


🏁 Script executed:

# Check if there are any TypeScript compilation errors in tests
cat test/unit/check-scripts.test.ts | head -100

Repository: nuxt/scripts

Length of output: 4017


🏁 Script executed:

# Let's verify if the string branch exists in UseScriptInput by checking unhead docs
web_search

Repository: nuxt/scripts

Length of output: 106


🌐 Web query:

@unhead/vue UseScriptInput type definition structure

πŸ’‘ Result:

In @unhead/vue (v2.1.4), UseScriptInput is defined as:

type UseScriptInput =
  | string
  | (ResolvableProperties<
      Omit<ScriptWithoutEvents & DataKeys & SchemaAugmentations['script'], 'src'>
    > & { src: string })

[1]

So you can pass either:

  1. A string β†’ treated as the src URL (e.g. useScript('https://…/sdk.js')). [1]
  2. An object with:
  • src: string (required)
  • any other <script> fields from Unhead’s script schema (minus src), plus Unhead data-* keys / schema augmentations, etc.
  • where each property is β€œresolvable” via ResolvableProperties<T> (i.e., values can be plain values, Ref, ComputedRef, or a getter function). [1][2]

ResolvableProperties is (simplified) mapped as:

type ResolvableProperties<T> = { [key in keyof T]?: ResolvableValue<T[key]> }

and ResolvableValue<T> includes T | (() => T) | Ref<T> | ComputedRef<T> (plus falsy allowances). [2]

Example

useScript({
  src: 'https://example.com/sdk.js',
  async: true,
  defer: () => true,
  // ...other script attributes supported by Unhead
})

[1][2]

Sources: @unhead/vue/dist/scripts.d.mts (UseScriptInput) [1], @unhead/vue/dist/shared/vue.DoxLTFJk.d.mts (ResolvableProperties/ResolvableValue) [2].


🏁 Script executed:

# Let's also check if there's a test that validates the typing of useScript with strings
rg "useScript.*string" test/ --type ts -B 3 -A 3

Repository: nuxt/scripts

Length of output: 489


🏁 Script executed:

# Check if there's a TypeScript compilation step that would catch this
cat tsconfig.json 2>/dev/null || cat tsconfig.app.json 2>/dev/null || echo "No tsconfig found in root"

Repository: nuxt/scripts

Length of output: 387


Preserve bare-string useScript() calls in the type signature.

The NuxtUseScriptInput type definition narrows away the string branch from UseScriptInput. In TypeScript, the intersection (string | Object) & Record<string & {}, any> distributes to (string & Record<string & {}, any>) | (Object & Record<string & {}, any>), and since string cannot intersect with an object type, the string branch is eliminated. This breaks all existing useScript('https://...') calls at compile time, even though the runtime at line 36 still normalizes string input. The pattern is widely used throughout tests and documentation.

Explicitly preserve the string union in NuxtUseScriptInput:

Suggested fix in src/runtime/types.ts
- export type NuxtUseScriptInput = UseScriptInput & Record<string & {}, any>
+ type ObjectUseScriptInput = Exclude<UseScriptInput, string>
+ export type NuxtUseScriptInput = string | (ObjectUseScriptInput & Record<string & {}, any>)
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/composables/useScript.ts` around lines 35 - 36, The type
NuxtUseScriptInput has been defined so it narrows away the string branch, which
breaks existing useScript('...') calls even though useScript normalizes string
at runtime; update the NuxtUseScriptInput type to explicitly preserve the string
union (e.g., NuxtUseScriptInput = string | (UseScriptInput & Record<string,
unknown>)) so the overload/union keeps bare-string calls while still accepting
the object formβ€”adjust the exported NuxtUseScriptInput type and any related
imports/exports to use this union without changing the runtime normalization in
useScript.

options = defu(options, useNuxtScriptRuntimeConfig()?.defaultScriptOptions) as NuxtUseScriptOptions<T>

Expand Down
2 changes: 1 addition & 1 deletion src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export type NuxtUseScriptOptions<T extends Record<symbol | string, any> = {}> =

export type NuxtUseScriptOptionsSerializable = Omit<NuxtUseScriptOptions, 'use' | 'skipValidation' | 'stub' | 'trigger' | 'eventContext' | 'beforeInit'> & { trigger?: 'client' | 'server' | 'onNuxtReady' | { idleTimeout: number } | { interaction: string[] } | { serviceWorker: true } }

export type NuxtUseScriptInput = UseScriptInput
export type NuxtUseScriptInput = UseScriptInput & Record<string & {}, string | boolean | number | undefined>

export interface TrackedPage {
title?: string
Expand Down
10 changes: 5 additions & 5 deletions src/runtime/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type {
EmptyOptionsSchema,
InferIfSchema,
NuxtUseScriptInput,
NuxtUseScriptOptions,
RegistryScriptInput,
ScriptRegistry,
UseFunctionType,
UseScriptContext,
} from '#nuxt-scripts/types'
import type { UseScriptInput } from '@unhead/vue'
import type { GenericSchema, InferInput, ObjectSchema, UnionSchema, ValiError } from 'valibot'
import { parse } from '#nuxt-scripts-validator'
import { defu } from 'defu'
Expand All @@ -30,8 +30,8 @@ function validateScriptInputSchema<T extends GenericSchema>(key: string, schema:
return null
}

type OptionsFn<O> = (options: InferIfSchema<O>, ctx: { scriptInput?: UseScriptInput & { src?: string } }) => ({
scriptInput?: UseScriptInput
type OptionsFn<O> = (options: InferIfSchema<O>, ctx: { scriptInput?: NuxtUseScriptInput & { src?: string } }) => ({
scriptInput?: NuxtUseScriptInput
scriptOptions?: NuxtUseScriptOptions
schema?: O extends ObjectSchema<any, any> | UnionSchema<any, any> ? O : undefined
clientInit?: () => void | Promise<any>
Expand All @@ -45,7 +45,7 @@ export function scriptRuntimeConfig<T extends keyof ScriptRegistry>(key: T) {
export function useRegistryScript<T extends Record<string | symbol, any>, O = EmptyOptionsSchema>(registryKey: keyof ScriptRegistry | string, optionsFn: OptionsFn<O>, _userOptions?: RegistryScriptInput<O>): UseScriptContext<UseFunctionType<NuxtUseScriptOptions<T>, T>> {
const scriptConfig = scriptRuntimeConfig(registryKey as keyof ScriptRegistry)
const userOptions = Object.assign(_userOptions || {}, typeof scriptConfig === 'object' ? scriptConfig : {})
const options = optionsFn(userOptions as InferIfSchema<O>, { scriptInput: userOptions.scriptInput as UseScriptInput & { src?: string } })
const options = optionsFn(userOptions as InferIfSchema<O>, { scriptInput: userOptions.scriptInput as NuxtUseScriptInput & { src?: string } })

// NEW: Handle NPM-only scripts differently
if (options.scriptMode === 'npm') {
Expand Down Expand Up @@ -82,7 +82,7 @@ export function useRegistryScript<T extends Record<string | symbol, any>, O = Em
}
}

const scriptInput = defu(finalScriptInput, userOptions.scriptInput, { key: registryKey }) as any as UseScriptInput
const scriptInput = defu(finalScriptInput, userOptions.scriptInput, { key: registryKey }) as any as NuxtUseScriptInput
const scriptOptions = Object.assign(userOptions?.scriptOptions || {}, options.scriptOptions || {})
if (import.meta.dev) {
// Capture where the component was loaded from
Expand Down
Loading