UNPKG

@nuxt/scripts

Version:

Load third-party scripts with better performance, privacy and DX in Nuxt Apps.

821 lines (809 loc) 35.2 kB
import { useNuxt, extendViteConfig, useLogger, addDevServerHandler, tryUseNuxt, logger as logger$1, defineNuxtModule, createResolver, addImports, addComponentsDir, addTemplate, addTypeTemplate, addPluginTemplate, addBuildPlugin, hasNuxtModule } from '@nuxt/kit'; import { defu } from 'defu'; import { resolvePackageJSON, readPackageJSON } from 'pkg-types'; import { existsSync } from 'node:fs'; import fsp from 'node:fs/promises'; import { createUnplugin } from 'unplugin'; import MagicString from 'magic-string'; import { asyncWalk, walk } from 'estree-walker'; import { joinURL, parseURL, parseQuery, hasProtocol } from 'ufo'; import { hash } from 'ohash'; import { join, resolve, relative } from 'pathe'; import { colors } from 'consola/utils'; import { fetch, $fetch } from 'ofetch'; import { lazyEventHandler, eventHandler, createError } from 'h3'; import { createStorage } from 'unstorage'; import fsDriver from 'unstorage/drivers/fs-lite'; import { pathToFileURL } from 'node:url'; import { isCI, provider } from 'std-env'; import { registry } from './registry.mjs'; const DEVTOOLS_UI_ROUTE = "/__nuxt-scripts"; const DEVTOOLS_UI_LOCAL_PORT = 3300; async function setupDevToolsUI(options, resolve, nuxt = useNuxt()) { const clientPath = await resolve("./client"); const isProductionBuild = existsSync(clientPath); if (isProductionBuild) { nuxt.hook("vite:serverCreated", async (server) => { const sirv = await import('sirv').then((r) => r.default || r); server.middlewares.use( DEVTOOLS_UI_ROUTE, sirv(clientPath, { dev: true, single: true }) ); }); } else { extendViteConfig((config) => { config.server = config.server || {}; config.server.proxy = config.server.proxy || {}; config.server.proxy[DEVTOOLS_UI_ROUTE] = { target: `http://localhost:${DEVTOOLS_UI_LOCAL_PORT}${DEVTOOLS_UI_ROUTE}`, changeOrigin: true, followRedirects: true, rewrite: (path) => path.replace(DEVTOOLS_UI_ROUTE, "") }; }); } nuxt.hook("devtools:customTabs", (tabs) => { tabs.push({ // unique identifier name: "nuxt-scripts", // title to display in the tab title: "Scripts", // any icon from Iconify, or a URL to an image icon: "carbon:script", // iframe view view: { type: "iframe", src: DEVTOOLS_UI_ROUTE } }); }); } const logger = useLogger("@nuxt/scripts"); const renderedScript = /* @__PURE__ */ new Map(); const ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365; const bundleStorage = () => { const nuxt = tryUseNuxt(); return createStorage({ driver: fsDriver({ base: resolve(nuxt?.options.rootDir || "", "node_modules/.cache/nuxt/scripts") }) }); }; function setupPublicAssetStrategy(options = {}) { const assetsBaseURL = options.prefix || "/_scripts"; const nuxt = useNuxt(); const storage = bundleStorage(); addDevServerHandler({ route: assetsBaseURL, handler: lazyEventHandler(async () => { return eventHandler(async (event) => { const filename = event.path.slice(1); const scriptDescriptor = renderedScript.get(join(assetsBaseURL, event.path.slice(1))); if (!scriptDescriptor || scriptDescriptor instanceof Error) throw createError({ statusCode: 404 }); const key = `bundle:${filename}`; let res = await storage.getItemRaw(key); if (!res) { res = await fetch(scriptDescriptor.src).then((r) => r.arrayBuffer()).then((r) => Buffer.from(r)); await storage.setItemRaw(key, res); } return res; }); }) }); if (nuxt.options.dev) { nuxt.options.routeRules ||= {}; nuxt.options.routeRules[joinURL(assetsBaseURL, "**")] = { cache: { maxAge: ONE_YEAR_IN_SECONDS } }; } nuxt.options.nitro.publicAssets ||= []; const cacheDir = join(nuxt.options.buildDir, "cache", "scripts"); nuxt.options.nitro.publicAssets.push(); nuxt.options.nitro = defu(nuxt.options.nitro, { publicAssets: [{ dir: cacheDir, maxAge: ONE_YEAR_IN_SECONDS, baseURL: assetsBaseURL }], prerender: { ignore: [assetsBaseURL] } }); return { renderedScript }; } function isVue(id, opts = {}) { const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href)); if (id.endsWith(".vue") && !search) { return true; } if (!search) { return false; } const query = parseQuery(search); if (query.nuxt_component) { return false; } if (query.macro && (search === "?macro=true" || !opts.type || opts.type.includes("script"))) { return true; } const type = "setup" in query ? "script" : query.type; if (!("vue" in query) || opts.type && !opts.type.includes(type)) { return false; } return true; } const JS_RE = /\.(?:[cm]?j|t)sx?$/; function isJS(id) { const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href)); return JS_RE.test(pathname); } const SEVEN_DAYS_IN_MS = 7 * 24 * 60 * 60 * 1e3; async function isCacheExpired(storage, filename, cacheMaxAge = SEVEN_DAYS_IN_MS) { const metaKey = `bundle-meta:${filename}`; const meta = await storage.getItem(metaKey); if (!meta || !meta.timestamp) { return true; } return Date.now() - meta.timestamp > cacheMaxAge; } function normalizeScriptData(src, assetsBaseURL = "/_scripts") { if (hasProtocol(src, { acceptRelative: true })) { src = src.replace(/^\/\//, "https://"); const url = parseURL(src); const file = [ `${hash(url)}.js` // force an extension ].filter(Boolean).join("-"); const nuxt = tryUseNuxt(); const cdnURL = nuxt?.options.runtimeConfig?.app?.cdnURL || nuxt?.options.app?.cdnURL || ""; const baseURL = cdnURL || nuxt?.options.app.baseURL || ""; return { url: joinURL(joinURL(baseURL, assetsBaseURL), file), filename: file }; } return { url: src }; } async function downloadScript(opts, renderedScript, fetchOptions, cacheMaxAge) { const { src, url, filename, forceDownload } = opts; if (src === url || !filename) { return; } const storage = bundleStorage(); const scriptContent = renderedScript.get(src); let res = scriptContent instanceof Error ? void 0 : scriptContent?.content; if (!res) { const cacheKey = `bundle:${filename}`; const shouldUseCache = !forceDownload && await storage.hasItem(cacheKey) && !await isCacheExpired(storage, filename, cacheMaxAge); if (shouldUseCache) { const res2 = await storage.getItemRaw(cacheKey); renderedScript.set(url, { content: res2, size: res2.length / 1024, encoding: "utf-8", src, filename }); return; } let encoding; let size = 0; res = await $fetch.raw(src, { ...fetchOptions, responseType: "arrayBuffer" }).then(async (r) => { if (!r.ok) { throw new Error(`Failed to fetch ${src} (HTTP ${r.status})`); } encoding = r.headers.get("content-encoding"); const contentLength = r.headers.get("content-length"); size = contentLength ? Number(contentLength) / 1024 : 0; return Buffer.from(r._data || await r.arrayBuffer()); }); await storage.setItemRaw(`bundle:${filename}`, res); await storage.setItem(`bundle-meta:${filename}`, { timestamp: Date.now(), src, filename }); size = size || res.length / 1024; logger.info(`Downloading script ${colors.gray(`${src} \u2192 ${filename} (${size.toFixed(2)} kB ${encoding})`)}`); renderedScript.set(url, { content: res, size, encoding, src, filename }); } } function NuxtScriptBundleTransformer(options = { renderedScript: /* @__PURE__ */ new Map() }) { const nuxt = useNuxt(); const { renderedScript = /* @__PURE__ */ new Map() } = options; const cacheDir = join(nuxt.options.buildDir, "cache", "scripts"); nuxt.hooks.hook("build:done", async () => { if (nuxt.options._prepare) { return; } const scripts = [...renderedScript]; if (!scripts.length) { logger.debug("[bundle-script-transformer] No scripts to bundle..."); return; } logger.debug("[bundle-script-transformer] Bundling scripts..."); if (!nuxt.options.dev) { await fsp.rm(cacheDir, { recursive: true, force: true }); } await fsp.mkdir(cacheDir, { recursive: true }); await Promise.all(scripts.map(async ([url, content]) => { if (content instanceof Error || !content.filename) return; await fsp.writeFile(join(nuxt.options.buildDir, "cache", "scripts", content.filename), content.content); logger.debug(colors.gray(` \u251C\u2500 ${url} \u2192 ${joinURL(content.src)} (${content.size.toFixed(2)} kB ${content.encoding})`)); })); }); return createUnplugin(() => { return { name: "nuxt:scripts:bundler-transformer", transformInclude(id) { return isVue(id, { type: ["template", "script"] }) || isJS(id); }, async transform(code, id) { if (!code.includes("useScript")) return; const ast = this.parse(code); const s = new MagicString(code); await asyncWalk(ast, { async enter(_node) { const calleeName = _node.callee?.name; if (!calleeName) return; const isValidCallee = calleeName === "useScript" || calleeName?.startsWith("useScript") && /^[A-Z]$/.test(calleeName?.charAt(9)) && !calleeName.startsWith("useScriptTrigger") && !calleeName.startsWith("useScriptEvent"); if (_node.type === "CallExpression" && _node.callee.type === "Identifier" && isValidCallee) { const fnName = _node.callee?.name; const node = _node; let scriptSrcNode; let src; if (fnName === "useScript") { if (node.arguments[0]?.type === "Literal") { scriptSrcNode = node.arguments[0]; } else if (node.arguments[0]?.type === "ObjectExpression") { const srcProperty = node.arguments[0].properties.find( (p) => (p.key?.name === "src" || p.key?.value === "src") && p?.value.type === "Literal" ); scriptSrcNode = srcProperty?.value; } } else { const registryNode = options.scripts?.find((i) => i.import.name === fnName); if (!registryNode) { return; } if (!registryNode.scriptBundling && !registryNode.src) return; const baseName = fnName.replace(/^useScript/, ""); const registryKey = baseName.length > 0 ? baseName.charAt(0).toLowerCase() + baseName.slice(1) : ""; const registryConfig = options.registryConfig?.[registryKey] || {}; const fnArg0 = {}; if (node.arguments[0]?.type === "ObjectExpression") { const optionsNode = node.arguments[0]; for (const prop of optionsNode.properties) { if (prop.type === "Property" && prop.value.type === "Literal" && prop.key && "name" in prop.key) fnArg0[prop.key.name] = prop.value.value; } const srcProperty = node.arguments[0].properties.find( (p) => (p.key?.name === "src" || p.key?.value === "src") && p?.value.type === "Literal" && p.type === "Property" ); if (srcProperty?.value?.value) { scriptSrcNode = srcProperty?.value; } } if (!scriptSrcNode) { const mergedOptions = { ...registryConfig, ...fnArg0 }; src = registryNode.scriptBundling && registryNode.scriptBundling(mergedOptions); if (src === false) return; } } if (!scriptSrcNode && !src) { const hasBundleOption = node.arguments[1]?.type === "ObjectExpression" && node.arguments[1].properties.some( (p) => (p.key?.name === "bundle" || p.key?.value === "bundle") && p.type === "Property" ); if (hasBundleOption) { const scriptOptionsArg = node.arguments[1]; const bundleProperty = scriptOptionsArg.properties.find( (p) => (p.key?.name === "bundle" || p.key?.value === "bundle") && p.type === "Property" ); if (bundleProperty && bundleProperty.value.type === "Literal") { const bundleValue = bundleProperty.value.value; if (bundleValue === true || bundleValue === "force" || String(bundleValue) === "true") { const valueNode = bundleProperty.value; s.overwrite(valueNode.start, valueNode.end, `'unsupported'`); } } } return; } if (scriptSrcNode || src) { src = src || (typeof scriptSrcNode?.value === "string" ? scriptSrcNode?.value : false); if (src) { let canBundle = options.defaultBundle === true || options.defaultBundle === "force"; let forceDownload = options.defaultBundle === "force"; if (node.arguments[1]?.type === "ObjectExpression") { const scriptOptionsArg = node.arguments[1]; const bundleProperty = scriptOptionsArg.properties.find( (p) => (p.key?.name === "bundle" || p.key?.value === "bundle") && p.type === "Property" ); if (bundleProperty && bundleProperty.value.type === "Literal") { const value = bundleProperty.value; const bundleValue = value.value; if (bundleValue !== true && bundleValue !== "force" && String(bundleValue) !== "true") { canBundle = false; return; } if (scriptOptionsArg.properties.length === 1) { s.remove(scriptOptionsArg.start, scriptOptionsArg.end); } else { const nextProperty = scriptOptionsArg.properties.find( (p) => p.start > bundleProperty.end && p.type === "Property" ); s.remove(bundleProperty.start, nextProperty ? nextProperty.start : bundleProperty.end); } canBundle = true; forceDownload = bundleValue === "force"; } } const scriptOptions = node.arguments[0]?.properties?.find( (p) => p.key?.name === "scriptOptions" ); const bundleOption = scriptOptions?.value.properties?.find((prop) => { return prop.type === "Property" && prop.key?.name === "bundle" && prop.value.type === "Literal"; }); if (bundleOption) { const bundleValue = bundleOption.value.value; canBundle = bundleValue === true || bundleValue === "force" || String(bundleValue) === "true"; forceDownload = bundleValue === "force"; } if (canBundle) { const { url: _url, filename } = normalizeScriptData(src, options.assetsBaseURL); let url = _url; try { await downloadScript({ src, url, filename, forceDownload }, renderedScript, options.fetchOptions, options.cacheMaxAge); } catch (e) { if (options.fallbackOnSrcOnBundleFail) { logger.warn(`[Nuxt Scripts: Bundle Transformer] Failed to bundle ${src}. Fallback to remote loading.`); url = src; } else { const errorMessage = e?.message || "Unknown error"; if (errorMessage.includes("timeout") || errorMessage.includes("network") || errorMessage.includes("ENOTFOUND")) { logger.error(`[Nuxt Scripts: Bundle Transformer] Network issue while bundling ${src}: ${errorMessage}`); logger.error(`[Nuxt Scripts: Bundle Transformer] Tip: Set 'fallbackOnSrcOnBundleFail: true' in module options or disable bundling in Docker environments`); } throw e; } } if (src === url) { if (src && src.startsWith("/")) logger.warn(`[Nuxt Scripts: Bundle Transformer] Relative scripts are already bundled. Skipping bundling for \`${src}\`.`); else logger.warn(`[Nuxt Scripts: Bundle Transformer] Failed to bundle ${src}.`); } if (scriptSrcNode) { s.overwrite(scriptSrcNode.start, scriptSrcNode.end, `'${url}'`); } else { if (node.arguments[0]) { const optionsNode = node.arguments[0]; const scriptInputProperty = optionsNode.properties.find( (p) => p.key?.name === "scriptInput" || p.key?.value === "scriptInput" ); if (scriptInputProperty) { const scriptInput = scriptInputProperty.value; if (scriptInput.type === "ObjectExpression") { const srcProperty = scriptInput.properties.find( (p) => p.key?.name === "src" || p.key?.value === "src" ); if (srcProperty) s.overwrite(srcProperty.value.start, srcProperty.value.end, `'${url}'`); else s.appendRight(scriptInput.end, `, src: '${url}'`); } } else { s.appendRight(node.arguments[0].start + 1, ` scriptInput: { src: '${url}' }, `); } } else { s.appendRight(node.callee.end, `({ scriptInput: { src: '${url}' } })`); } } } } } } } }); if (s.hasChanged()) { return { code: s.toString(), map: s.generateMap({ includeContent: true, source: id }) }; } } }; }); } const isStackblitz = provider === "stackblitz"; async function promptToInstall(name, installCommand, options) { if (await resolvePackageJSON(name).catch(() => null)) return true; logger$1.info(`Package ${name} is missing`); if (isCI) return false; if (options.prompt === true || options.prompt !== false && !isStackblitz) { const confirm = await logger$1.prompt(`Do you want to install ${name} package?`, { type: "confirm", name: "confirm", initial: true }); if (!confirm) return false; } logger$1.info(`Installing ${name}...`); try { await installCommand(); logger$1.success(`Installed ${name}`); return true; } catch (err) { logger$1.error(err); return false; } } const installPrompts = /* @__PURE__ */ new Set(); function installNuxtModule(name, options) { if (installPrompts.has(name)) return; installPrompts.add(name); const nuxt = tryUseNuxt(); if (!nuxt) return; return promptToInstall(name, async () => { const { runCommand } = await import(String("nuxi")); await runCommand("module", ["add", name, "--cwd", nuxt.options.rootDir]); }, { rootDir: nuxt.options.rootDir, searchPaths: nuxt.options.modulesDir, ...options }); } function NuxtScriptsCheckScripts() { return createUnplugin(() => { return { name: "nuxt-scripts:check-scripts", transformInclude(id) { return isVue(id, { type: ["script"] }); }, async transform(code) { if (!code.includes("useScript")) return; const ast = this.parse(code); let nameNode; let errorNode; walk(ast, { enter(_node) { if (_node.type === "VariableDeclaration" && _node.declarations?.[0]?.id?.type === "ObjectPattern") { const objPattern = _node.declarations[0]?.id; for (const property of objPattern.properties) { if (property.type === "Property" && property.key.type === "Identifier" && property.key.name === "$script" && property.value.type === "Identifier") { nameNode = _node; } } } if (nameNode) { let sequence = _node.type === "SequenceExpression" ? _node : null; let assignmentExpression; if (_node.type === "VariableDeclaration") { if (_node.declarations[0]?.init?.type === "SequenceExpression") { sequence = _node.declarations[0]?.init; assignmentExpression = _node.declarations[0]?.init?.expressions?.[0]; } } if (sequence && !assignmentExpression) { assignmentExpression = sequence.expressions[0]?.type === "AssignmentExpression" ? sequence.expressions[0] : null; } if (assignmentExpression) { const right = assignmentExpression?.right; if (right.callee?.name === "_withAsyncContext") { if (right.arguments[0]?.body?.name === "$script" || right.arguments[0]?.body?.callee?.object?.name === "$script") { errorNode = nameNode; } } } } } }); if (errorNode) { return this.error(new Error("You can't use a top-level await on $script as it will never resolve.")); } } }; }); } function templateTriggerResolver(defaultScriptOptions) { const needsIdleTimeout = defaultScriptOptions?.trigger && typeof defaultScriptOptions.trigger === "object" && "idleTimeout" in defaultScriptOptions.trigger; const needsInteraction = defaultScriptOptions?.trigger && typeof defaultScriptOptions.trigger === "object" && "interaction" in defaultScriptOptions.trigger; const imports = []; if (needsIdleTimeout) { imports.push(`import { useScriptTriggerIdleTimeout } from '#nuxt-scripts/composables/useScriptTriggerIdleTimeout'`); } if (needsInteraction) { imports.push(`import { useScriptTriggerInteraction } from '#nuxt-scripts/composables/useScriptTriggerInteraction'`); } return [ ...imports, `export function resolveTrigger(trigger) {`, needsIdleTimeout ? ` if ('idleTimeout' in trigger) return useScriptTriggerIdleTimeout({ timeout: trigger.idleTimeout })` : "", needsInteraction ? ` if ('interaction' in trigger) return useScriptTriggerInteraction({ events: trigger.interaction })` : "", ` return null`, `}` ].filter(Boolean).join("\n"); } function resolveTriggerForTemplate(trigger) { if (trigger && typeof trigger === "object") { const keys = Object.keys(trigger); if (keys.length > 1) { throw new Error(`Trigger object must have exactly one property, received: ${keys.join(", ")}`); } if ("idleTimeout" in trigger) { return `useScriptTriggerIdleTimeout({ timeout: ${trigger.idleTimeout} })`; } if ("interaction" in trigger) { return `useScriptTriggerInteraction({ events: ${JSON.stringify(trigger.interaction)} })`; } } return null; } function templatePlugin(config, registry) { if (Array.isArray(config.globals)) { config.globals = Object.fromEntries(config.globals.map((i) => [hash(i), i])); logger.warn("The `globals` array option is deprecated, please convert to an object."); } const imports = []; const inits = []; let needsIdleTimeoutImport = false; let needsInteractionImport = false; for (const [k, c] of Object.entries(config.registry || {})) { const importDefinition = registry.find((i) => i.import.name === `useScript${k.substring(0, 1).toUpperCase() + k.substring(1)}`); if (importDefinition) { imports.unshift(`import { ${importDefinition.import.name} } from '${importDefinition.import.from}'`); const args = (typeof c !== "object" ? {} : c) || {}; if (c === "mock") { args.scriptOptions = { trigger: "manual", skipValidation: true }; } else if (Array.isArray(c) && c.length === 2 && c[1]?.trigger) { const triggerResolved = resolveTriggerForTemplate(c[1].trigger); if (triggerResolved) { args.scriptOptions = { ...c[1] }; if (args.scriptOptions) { args.scriptOptions.trigger = `__TRIGGER_${triggerResolved}__`; } if (triggerResolved.includes("useScriptTriggerIdleTimeout")) needsIdleTimeoutImport = true; if (triggerResolved.includes("useScriptTriggerInteraction")) needsInteractionImport = true; } } inits.push(`const ${k} = ${importDefinition.import.name}(${JSON.stringify(args).replace(/"__TRIGGER_(.*?)__"/g, "$1")})`); } } for (const [k, c] of Object.entries(config.globals || {})) { if (typeof c === "string") { inits.push(`const ${k} = useScript(${JSON.stringify({ src: c, key: k })}, { use: () => ({ ${k}: window.${k} }) })`); } else if (Array.isArray(c) && c.length === 2) { const options = c[1]; const triggerResolved = resolveTriggerForTemplate(options?.trigger); if (triggerResolved) { if (triggerResolved.includes("useScriptTriggerIdleTimeout")) needsIdleTimeoutImport = true; if (triggerResolved.includes("useScriptTriggerInteraction")) needsInteractionImport = true; const resolvedOptions = { ...options, trigger: `__TRIGGER_${triggerResolved}__` }; inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...typeof c[0] === "string" ? { src: c[0] } : c[0] })}, { ...${JSON.stringify(resolvedOptions).replace(/"__TRIGGER_(.*?)__"/g, "$1")}, use: () => ({ ${k}: window.${k} }) })`); } else { inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...typeof c[0] === "string" ? { src: c[0] } : c[0] })}, { ...${JSON.stringify(c[1])}, use: () => ({ ${k}: window.${k} }) })`); } } else if (typeof c === "object" && c !== null) { const triggerResolved = resolveTriggerForTemplate(c.trigger); if (triggerResolved) { if (triggerResolved.includes("useScriptTriggerIdleTimeout")) needsIdleTimeoutImport = true; if (triggerResolved.includes("useScriptTriggerInteraction")) needsInteractionImport = true; const resolvedOptions = { ...c, trigger: `__TRIGGER_${triggerResolved}__` }; inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...resolvedOptions }).replace(/"__TRIGGER_(.*?)__"/g, "$1")}, { use: () => ({ ${k}: window.${k} }) })`); } else { inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...c })}, { use: () => ({ ${k}: window.${k} }) })`); } } } const triggerImports = []; if (needsIdleTimeoutImport) { triggerImports.push(`import { useScriptTriggerIdleTimeout } from '#nuxt-scripts/composables/useScriptTriggerIdleTimeout'`); } if (needsInteractionImport) { triggerImports.push(`import { useScriptTriggerInteraction } from '#nuxt-scripts/composables/useScriptTriggerInteraction'`); } return [ `import { useScript } from '#nuxt-scripts/composables/useScript'`, `import { defineNuxtPlugin } from 'nuxt/app'`, ...triggerImports, ...imports, "", `export default defineNuxtPlugin({`, ` name: "scripts:init",`, ` env: { islands: false },`, ` parallel: true,`, ` setup() {`, ...inits.map((i) => ` ${i}`), ` return { provide: { $scripts: { ${[...Object.keys(config.globals || {}), ...Object.keys(config.registry || {})].join(", ")} } } }`, ` }`, `})` ].join("\n"); } const module$1 = defineNuxtModule({ meta: { name: "@nuxt/scripts", configKey: "scripts", compatibility: { nuxt: ">=3.16" } }, defaults: { defaultScriptOptions: { trigger: "onNuxtReady" }, assets: { fetchOptions: { retry: 3, // Specifies the number of retry attempts for failed fetches. retryDelay: 2e3, // Specifies the delay (in milliseconds) between retry attempts. timeout: 15e3 // Configures the maximum time (in milliseconds) allowed for each fetch attempt. } }, enabled: true, debug: false }, async setup(config, nuxt) { const { resolvePath } = createResolver(import.meta.url); const { version, name } = await readPackageJSON(await resolvePath("../package.json")); nuxt.options.alias["#nuxt-scripts-validator"] = await resolvePath(`./runtime/validation/${nuxt.options.dev || nuxt.options._prepare ? "valibot" : "mock"}`); nuxt.options.alias["#nuxt-scripts"] = await resolvePath("./runtime"); logger.level = config.debug || nuxt.options.debug ? 4 : 3; if (!config.enabled) { logger.debug("The module is disabled, skipping setup."); return; } const { version: unheadVersion } = await readPackageJSON("@unhead/vue", { from: nuxt.options.modulesDir }).catch(() => ({ version: null })); if (unheadVersion?.startsWith("1")) { logger.error(`Nuxt Scripts requires Unhead >= 2, you are using v${unheadVersion}. Please run \`nuxi upgrade --clean\` to upgrade...`); } nuxt.options.runtimeConfig["nuxt-scripts"] = { version }; nuxt.options.runtimeConfig.public["nuxt-scripts"] = { // expose for devtools version: nuxt.options.dev ? version : void 0, defaultScriptOptions: config.defaultScriptOptions }; if (config.registry) { nuxt.options.runtimeConfig.public = nuxt.options.runtimeConfig.public || {}; nuxt.options.runtimeConfig.public.scripts = defu( nuxt.options.runtimeConfig.public.scripts || {}, config.registry ); } const composables = [ "useScript", "useScriptEventPage", "useScriptTriggerConsent", "useScriptTriggerElement", "useScriptTriggerIdleTimeout", "useScriptTriggerInteraction" ]; for (const composable of composables) { addImports({ priority: 2, name: composable, as: composable, from: await resolvePath(`./runtime/composables/${composable}`) }); } addComponentsDir({ path: await resolvePath("./runtime/components"), pathPrefix: false }); addTemplate({ filename: "nuxt-scripts-trigger-resolver.mjs", getContents() { return templateTriggerResolver(config.defaultScriptOptions); } }); const scripts = await registry(resolvePath); for (const script of scripts) { if (script.import?.name) { addImports({ priority: 2, ...script.import }); script._importRegistered = true; } } nuxt.hooks.hook("modules:done", async () => { const registryScripts = [...scripts]; await nuxt.hooks.callHook("scripts:registry", registryScripts); for (const script of registryScripts) { if (script.import?.name && !script._importRegistered) { addImports({ priority: 3, ...script.import }); } } const registryScriptsWithImport = registryScripts.filter((i) => !!i.import?.name); const newScripts = registryScriptsWithImport.filter((i) => !scripts.some((r) => r.import?.name === i.import.name)); addTypeTemplate({ filename: "module/nuxt-scripts.d.ts", getContents: (data) => { const typesPath = relative(resolve(data.nuxt.options.rootDir, data.nuxt.options.buildDir, "module"), resolve("runtime/types")); let types = ` declare module '#app' { interface NuxtApp { $scripts: Record<${[...Object.keys(config.globals || {}), ...Object.keys(config.registry || {})].map((k) => `'${k}'`).concat(["string"]).join(" | ")}, (import('#nuxt-scripts/types').UseScriptContext<any>)> _scripts: Record<string, (import('#nuxt-scripts/types').UseScriptContext<any>)> } interface RuntimeNuxtHooks { 'scripts:updated': (ctx: { scripts: Record<string, (import('#nuxt-scripts/types').UseScriptContext<any>)> }) => void | Promise<void> } } `; if (newScripts.length) { types = `${types} declare module '#nuxt-scripts/types' { type NuxtUseScriptOptions = Omit<import('${typesPath}').NuxtUseScriptOptions, 'use' | 'beforeInit'> interface ScriptRegistry { ${newScripts.map((i) => { const key = i.import?.name.replace("useScript", ""); const keyLcFirst = key.substring(0, 1).toLowerCase() + key.substring(1); return ` ${keyLcFirst}?: import('${i.import?.from}').${key}Input | [import('${i.import?.from}').${key}Input, NuxtUseScriptOptions]`; }).join("\n")} } }`; return types; } return `${types} export {}`; } }, { nuxt: true, node: true }); if (Object.keys(config.globals || {}).length || Object.keys(config.registry || {}).length) { addPluginTemplate({ filename: `modules/${name.replace("/", "-")}/plugin.mjs`, getContents() { return templatePlugin(config, registryScriptsWithImport); } }); } const { renderedScript } = setupPublicAssetStrategy(config.assets); const moduleInstallPromises = /* @__PURE__ */ new Map(); addBuildPlugin(NuxtScriptsCheckScripts(), { dev: true }); addBuildPlugin(NuxtScriptBundleTransformer({ scripts: registryScriptsWithImport, registryConfig: nuxt.options.runtimeConfig.public.scripts, defaultBundle: config.defaultScriptOptions?.bundle, moduleDetected(module) { if (nuxt.options.dev && module !== "@nuxt/scripts" && !moduleInstallPromises.has(module) && !hasNuxtModule(module)) moduleInstallPromises.set(module, () => installNuxtModule(module)); }, assetsBaseURL: config.assets?.prefix, fallbackOnSrcOnBundleFail: config.assets?.fallbackOnSrcOnBundleFail, fetchOptions: config.assets?.fetchOptions, cacheMaxAge: config.assets?.cacheMaxAge, renderedScript })); nuxt.hooks.hook("build:done", async () => { const initPromise = Array.from(moduleInstallPromises.values()); for (const p of initPromise) await p?.(); }); }); if (nuxt.options.dev) setupDevToolsUI(config, resolvePath); } }); export { module$1 as default };