@nuxt/scripts
Version:
Load third-party scripts with better performance, privacy and DX in Nuxt Apps.
821 lines (809 loc) • 35.2 kB
JavaScript
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 };