-
Notifications
You must be signed in to change notification settings - Fork 83
Expand file tree
/
Copy pathutils.ts
More file actions
157 lines (139 loc) · 6.18 KB
/
utils.ts
File metadata and controls
157 lines (139 loc) · 6.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import type {
EmptyOptionsSchema,
InferIfSchema,
NuxtUseScriptInput,
NuxtUseScriptOptions,
RegistryScriptInput,
ScriptRegistry,
UseFunctionType,
UseScriptContext,
} from '#nuxt-scripts/types'
import type { GenericSchema, InferInput, ObjectSchema, UnionSchema, ValiError } from 'valibot'
import { parse } from '#nuxt-scripts-validator'
import { defu } from 'defu'
import { useRuntimeConfig } from 'nuxt/app'
import { parseQuery, parseURL, withQuery } from 'ufo'
import { useScript } from './composables/useScript'
import { createNpmScriptStub } from './npm-script-stub'
export type MaybePromise<T> = Promise<T> | T
function validateScriptInputSchema<T extends GenericSchema>(key: string, schema: T, options?: InferInput<T>) {
if (import.meta.dev) {
try {
parse(schema, options)
}
catch (_e) {
return _e as ValiError<any>
}
}
return null
}
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>
scriptMode?: 'external' | 'npm' // NEW: external = CDN script (default), npm = NPM package only
})
export function scriptRuntimeConfig<T extends keyof ScriptRegistry>(key: T) {
return ((useRuntimeConfig().public.scripts || {}) as ScriptRegistry)[key]
}
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 NuxtUseScriptInput & { src?: string } })
// NEW: Handle NPM-only scripts differently
if (options.scriptMode === 'npm') {
return createNpmScriptStub<T>({
key: String(registryKey),
use: options.scriptOptions?.use,
clientInit: options.clientInit,
trigger: userOptions.scriptOptions?.trigger as any,
}) as any as UseScriptContext<UseFunctionType<NuxtUseScriptOptions<T>, T>>
}
let finalScriptInput = options.scriptInput
// If user provided a custom src and the options function returned a src with query params,
// extract those query params and apply them to the user's custom src
const userSrc = (userOptions.scriptInput as any)?.src
const optionsSrc = (options.scriptInput as any)?.src
if (userSrc && optionsSrc && typeof optionsSrc === 'string' && typeof userSrc === 'string') {
const defaultUrl = parseURL(optionsSrc)
const customUrl = parseURL(userSrc)
// Merge query params: user params override default params
const defaultQuery = parseQuery(defaultUrl.search || '')
const customQuery = parseQuery(customUrl.search || '')
const mergedQuery = { ...defaultQuery, ...customQuery }
// Build the final URL with the custom base and merged query params
const baseUrl = customUrl.href?.split('?')[0] || userSrc
finalScriptInput = {
...((options.scriptInput as object) || {}),
src: withQuery(baseUrl, mergedQuery),
}
}
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
const error = new Error('Stack trace for component location')
const stack = error.stack?.split('\n')
const callerLine = stack?.find(line =>
line.includes('.vue')
&& !line.includes('useRegistryScript')
&& !line.includes('node_modules'),
)
let loadedFrom = 'unknown'
if (callerLine) {
// Extract URL pattern like "https://localhost:3000/_nuxt/pages/features/custom-registry.vue?t=1758609859248:14:31"
// Handle both with and without query parameters
const urlMatch = callerLine.match(/https?:\/\/[^/]+\/_nuxt\/(.+\.vue)(?:\?[^)]*)?:(\d+):(\d+)/)
|| callerLine.match(/\(https?:\/\/[^/]+\/_nuxt\/(.+\.vue)(?:\?[^)]*)?:(\d+):(\d+)\)/)
if (urlMatch) {
const [, filePath, line, column] = urlMatch
loadedFrom = `./${filePath}:${line}:${column}`
}
else {
// Try to extract any .vue file path with line:column
const vueMatch = callerLine.match(/([^/\s]+\.vue):(\d+):(\d+)/)
if (vueMatch) {
const [, fileName, line, column] = vueMatch
loadedFrom = `./${fileName}:${line}:${column}`
}
else {
// Fallback to original cleaning
loadedFrom = callerLine.trim().replace(/^\s*at\s+/, '')
}
}
}
scriptOptions.devtools = defu(scriptOptions.devtools, { registryKey, loadedFrom })
if (options.schema) {
const registryMeta: Record<string, string> = {}
const entries = 'entries' in options.schema ? options.schema.entries as Record<string, { type: string }> : undefined
for (const k in entries) {
if (entries[k]?.type !== 'optional') {
registryMeta[k] = String(userOptions[k as any as keyof typeof userOptions])
}
}
scriptOptions.devtools.registryMeta = registryMeta
}
}
const init = scriptOptions.beforeInit
if (import.meta.dev) {
scriptOptions._validate = () => {
// a manual trigger also means it was disabled by nuxt.config
// overriding the src will skip validation
if (!userOptions.scriptInput?.src && !scriptOptions.skipValidation && options.schema) {
return validateScriptInputSchema(registryKey, options.schema, userOptions)
}
}
}
// wrap beforeInit to add validation
scriptOptions.beforeInit = () => {
// avoid clearing the user beforeInit
init?.()
if (import.meta.client) {
// validate input in dev
options.clientInit?.()
}
}
return useScript<T>(scriptInput, scriptOptions as NuxtUseScriptOptions<T>)
}
export * from './utils/pure'