638 lines
20 KiB
JavaScript
638 lines
20 KiB
JavaScript
|
import { createHooks } from 'hookable';
|
||
|
import { DomPlugin } from '@unhead/dom';
|
||
|
import { defineHeadPlugin, tagDedupeKey, tagWeight, HasElementTags, hashCode, NetworkEvents, SortModifiers, processTemplateParams, resolveTitleTemplate, IsBrowser, normaliseEntryTags, composableNames, whitelistSafeInput, unpackMeta } from '@unhead/shared';
|
||
|
export { composableNames } from '@unhead/shared';
|
||
|
|
||
|
const UsesMergeStrategy = ["templateParams", "htmlAttrs", "bodyAttrs"];
|
||
|
const DedupePlugin = defineHeadPlugin({
|
||
|
hooks: {
|
||
|
"tag:normalise": function({ tag }) {
|
||
|
["hid", "vmid", "key"].forEach((key) => {
|
||
|
if (tag.props[key]) {
|
||
|
tag.key = tag.props[key];
|
||
|
delete tag.props[key];
|
||
|
}
|
||
|
});
|
||
|
const generatedKey = tagDedupeKey(tag);
|
||
|
const dedupe = generatedKey || (tag.key ? `${tag.tag}:${tag.key}` : false);
|
||
|
if (dedupe)
|
||
|
tag._d = dedupe;
|
||
|
},
|
||
|
"tags:resolve": function(ctx) {
|
||
|
const deduping = {};
|
||
|
ctx.tags.forEach((tag) => {
|
||
|
const dedupeKey = (tag.key ? `${tag.tag}:${tag.key}` : tag._d) || tag._p;
|
||
|
const dupedTag = deduping[dedupeKey];
|
||
|
if (dupedTag) {
|
||
|
let strategy = tag?.tagDuplicateStrategy;
|
||
|
if (!strategy && UsesMergeStrategy.includes(tag.tag))
|
||
|
strategy = "merge";
|
||
|
if (strategy === "merge") {
|
||
|
const oldProps = dupedTag.props;
|
||
|
["class", "style"].forEach((key) => {
|
||
|
if (tag.props[key] && oldProps[key]) {
|
||
|
if (key === "style" && !oldProps[key].endsWith(";"))
|
||
|
oldProps[key] += ";";
|
||
|
tag.props[key] = `${oldProps[key]} ${tag.props[key]}`;
|
||
|
}
|
||
|
});
|
||
|
deduping[dedupeKey].props = {
|
||
|
...oldProps,
|
||
|
...tag.props
|
||
|
};
|
||
|
return;
|
||
|
} else if (tag._e === dupedTag._e) {
|
||
|
dupedTag._duped = dupedTag._duped || [];
|
||
|
tag._d = `${dupedTag._d}:${dupedTag._duped.length + 1}`;
|
||
|
dupedTag._duped.push(tag);
|
||
|
return;
|
||
|
} else if (tagWeight(tag) > tagWeight(dupedTag)) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
const propCount = Object.keys(tag.props).length + (tag.innerHTML ? 1 : 0) + (tag.textContent ? 1 : 0);
|
||
|
if (HasElementTags.includes(tag.tag) && propCount === 0) {
|
||
|
delete deduping[dedupeKey];
|
||
|
return;
|
||
|
}
|
||
|
deduping[dedupeKey] = tag;
|
||
|
});
|
||
|
const newTags = [];
|
||
|
Object.values(deduping).forEach((tag) => {
|
||
|
const dupes = tag._duped;
|
||
|
delete tag._duped;
|
||
|
newTags.push(tag);
|
||
|
if (dupes)
|
||
|
newTags.push(...dupes);
|
||
|
});
|
||
|
ctx.tags = newTags;
|
||
|
ctx.tags = ctx.tags.filter((t) => !(t.tag === "meta" && (t.props.name || t.props.property) && !t.props.content));
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const PayloadPlugin = defineHeadPlugin({
|
||
|
mode: "server",
|
||
|
hooks: {
|
||
|
"tags:resolve": function(ctx) {
|
||
|
const payload = {};
|
||
|
ctx.tags.filter((tag) => ["titleTemplate", "templateParams", "title"].includes(tag.tag) && tag._m === "server").forEach((tag) => {
|
||
|
payload[tag.tag] = tag.tag.startsWith("title") ? tag.textContent : tag.props;
|
||
|
});
|
||
|
Object.keys(payload).length && ctx.tags.push({
|
||
|
tag: "script",
|
||
|
innerHTML: JSON.stringify(payload),
|
||
|
props: { id: "unhead:payload", type: "application/json" }
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const ValidEventTags = ["script", "link", "bodyAttrs"];
|
||
|
function stripEventHandlers(tag) {
|
||
|
const props = {};
|
||
|
const eventHandlers = {};
|
||
|
Object.entries(tag.props).forEach(([key, value]) => {
|
||
|
if (key.startsWith("on") && typeof value === "function") {
|
||
|
if (NetworkEvents.includes(key))
|
||
|
props[key] = `this.dataset.${key} = true`;
|
||
|
eventHandlers[key] = value;
|
||
|
} else {
|
||
|
props[key] = value;
|
||
|
}
|
||
|
});
|
||
|
return { props, eventHandlers };
|
||
|
}
|
||
|
const EventHandlersPlugin = defineHeadPlugin((head) => ({
|
||
|
hooks: {
|
||
|
"tags:resolve": function(ctx) {
|
||
|
for (const tag of ctx.tags) {
|
||
|
if (ValidEventTags.includes(tag.tag)) {
|
||
|
const { props, eventHandlers } = stripEventHandlers(tag);
|
||
|
tag.props = props;
|
||
|
if (Object.keys(eventHandlers).length) {
|
||
|
if (tag.props.src || tag.props.href)
|
||
|
tag.key = tag.key || hashCode(tag.props.src || tag.props.href);
|
||
|
tag._eventHandlers = eventHandlers;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
"dom:renderTag": function(ctx, dom, track) {
|
||
|
if (!ctx.tag._eventHandlers)
|
||
|
return;
|
||
|
const $eventListenerTarget = ctx.tag.tag === "bodyAttrs" ? dom.defaultView : ctx.$el;
|
||
|
Object.entries(ctx.tag._eventHandlers).forEach(([k, value]) => {
|
||
|
const sdeKey = `${ctx.tag._d || ctx.tag._p}:${k}`;
|
||
|
const eventName = k.slice(2).toLowerCase();
|
||
|
const eventDedupeKey = `data-h-${eventName}`;
|
||
|
track(ctx.id, sdeKey, () => {
|
||
|
});
|
||
|
if (ctx.$el.hasAttribute(eventDedupeKey))
|
||
|
return;
|
||
|
ctx.$el.setAttribute(eventDedupeKey, "");
|
||
|
let observer;
|
||
|
const handler = (e) => {
|
||
|
value(e);
|
||
|
observer?.disconnect();
|
||
|
};
|
||
|
if (k in ctx.$el.dataset) {
|
||
|
handler(new Event(k.replace("on", "")));
|
||
|
} else if (NetworkEvents.includes(k) && typeof MutationObserver !== "undefined") {
|
||
|
observer = new MutationObserver((e) => {
|
||
|
const hasAttr = e.some((m) => m.attributeName === `data-${k}`);
|
||
|
if (hasAttr) {
|
||
|
handler(new Event(k.replace("on", "")));
|
||
|
observer?.disconnect();
|
||
|
}
|
||
|
});
|
||
|
observer.observe(ctx.$el, {
|
||
|
attributes: true
|
||
|
});
|
||
|
} else {
|
||
|
$eventListenerTarget.addEventListener(eventName, handler);
|
||
|
}
|
||
|
track(ctx.id, sdeKey, () => {
|
||
|
observer?.disconnect();
|
||
|
$eventListenerTarget.removeEventListener(eventName, handler);
|
||
|
ctx.$el.removeAttribute(eventDedupeKey);
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}));
|
||
|
|
||
|
const DupeableTags = ["link", "style", "script", "noscript"];
|
||
|
const HashKeyedPlugin = defineHeadPlugin({
|
||
|
hooks: {
|
||
|
"tag:normalise": ({ tag }) => {
|
||
|
if (tag.key && DupeableTags.includes(tag.tag)) {
|
||
|
tag.props["data-hid"] = tag._h = hashCode(tag.key);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const SortPlugin = defineHeadPlugin({
|
||
|
hooks: {
|
||
|
"tags:resolve": (ctx) => {
|
||
|
const tagPositionForKey = (key) => ctx.tags.find((tag) => tag._d === key)?._p;
|
||
|
for (const { prefix, offset } of SortModifiers) {
|
||
|
for (const tag of ctx.tags.filter((tag2) => typeof tag2.tagPriority === "string" && tag2.tagPriority.startsWith(prefix))) {
|
||
|
const position = tagPositionForKey(
|
||
|
tag.tagPriority.replace(prefix, "")
|
||
|
);
|
||
|
if (typeof position !== "undefined")
|
||
|
tag._p = position + offset;
|
||
|
}
|
||
|
}
|
||
|
ctx.tags.sort((a, b) => a._p - b._p).sort((a, b) => tagWeight(a) - tagWeight(b));
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const SupportedAttrs = {
|
||
|
meta: "content",
|
||
|
link: "href",
|
||
|
htmlAttrs: "lang"
|
||
|
};
|
||
|
const TemplateParamsPlugin = defineHeadPlugin((head) => ({
|
||
|
hooks: {
|
||
|
"tags:resolve": (ctx) => {
|
||
|
const { tags } = ctx;
|
||
|
const title = tags.find((tag) => tag.tag === "title")?.textContent;
|
||
|
const idx = tags.findIndex((tag) => tag.tag === "templateParams");
|
||
|
const params = idx !== -1 ? tags[idx].props : {};
|
||
|
const sep = params.separator || "|";
|
||
|
delete params.separator;
|
||
|
params.pageTitle = processTemplateParams(params.pageTitle || title || "", params, sep);
|
||
|
for (const tag of tags.filter((t) => t.processTemplateParams !== false)) {
|
||
|
const v = SupportedAttrs[tag.tag];
|
||
|
if (v && typeof tag.props[v] === "string") {
|
||
|
tag.props[v] = processTemplateParams(tag.props[v], params, sep);
|
||
|
} else if (tag.processTemplateParams === true || ["titleTemplate", "title"].includes(tag.tag)) {
|
||
|
["innerHTML", "textContent"].forEach((p) => {
|
||
|
if (typeof tag[p] === "string")
|
||
|
tag[p] = processTemplateParams(tag[p], params, sep);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
head._templateParams = params;
|
||
|
head._separator = sep;
|
||
|
ctx.tags = tags.filter((tag) => tag.tag !== "templateParams");
|
||
|
}
|
||
|
}
|
||
|
}));
|
||
|
|
||
|
const TitleTemplatePlugin = defineHeadPlugin({
|
||
|
hooks: {
|
||
|
"tags:resolve": (ctx) => {
|
||
|
const { tags } = ctx;
|
||
|
let titleTemplateIdx = tags.findIndex((i) => i.tag === "titleTemplate");
|
||
|
const titleIdx = tags.findIndex((i) => i.tag === "title");
|
||
|
if (titleIdx !== -1 && titleTemplateIdx !== -1) {
|
||
|
const newTitle = resolveTitleTemplate(
|
||
|
tags[titleTemplateIdx].textContent,
|
||
|
tags[titleIdx].textContent
|
||
|
);
|
||
|
if (newTitle !== null) {
|
||
|
tags[titleIdx].textContent = newTitle || tags[titleIdx].textContent;
|
||
|
} else {
|
||
|
delete tags[titleIdx];
|
||
|
}
|
||
|
} else if (titleTemplateIdx !== -1) {
|
||
|
const newTitle = resolveTitleTemplate(
|
||
|
tags[titleTemplateIdx].textContent
|
||
|
);
|
||
|
if (newTitle !== null) {
|
||
|
tags[titleTemplateIdx].textContent = newTitle;
|
||
|
tags[titleTemplateIdx].tag = "title";
|
||
|
titleTemplateIdx = -1;
|
||
|
}
|
||
|
}
|
||
|
if (titleTemplateIdx !== -1) {
|
||
|
delete tags[titleTemplateIdx];
|
||
|
}
|
||
|
ctx.tags = tags.filter(Boolean);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const XSSPlugin = defineHeadPlugin({
|
||
|
hooks: {
|
||
|
"tags:afterResolve": function(ctx) {
|
||
|
for (const tag of ctx.tags) {
|
||
|
if (typeof tag.innerHTML === "string") {
|
||
|
if (tag.innerHTML && ["application/ld+json", "application/json"].includes(tag.props.type)) {
|
||
|
tag.innerHTML = tag.innerHTML.replace(/</g, "\\u003C");
|
||
|
} else {
|
||
|
tag.innerHTML = tag.innerHTML.replace(new RegExp(`</${tag.tag}`, "g"), `<\\/${tag.tag}`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
let activeHead;
|
||
|
// @__NO_SIDE_EFFECTS__
|
||
|
function createHead(options = {}) {
|
||
|
const head = createHeadCore(options);
|
||
|
head.use(DomPlugin());
|
||
|
return activeHead = head;
|
||
|
}
|
||
|
// @__NO_SIDE_EFFECTS__
|
||
|
function createServerHead(options = {}) {
|
||
|
return activeHead = createHeadCore(options);
|
||
|
}
|
||
|
function filterMode(mode, ssr) {
|
||
|
return !mode || mode === "server" && ssr || mode === "client" && !ssr;
|
||
|
}
|
||
|
function createHeadCore(options = {}) {
|
||
|
const hooks = createHooks();
|
||
|
hooks.addHooks(options.hooks || {});
|
||
|
options.document = options.document || (IsBrowser ? document : void 0);
|
||
|
const ssr = !options.document;
|
||
|
const updated = () => {
|
||
|
head.dirty = true;
|
||
|
hooks.callHook("entries:updated", head);
|
||
|
};
|
||
|
let entryCount = 0;
|
||
|
let entries = [];
|
||
|
const plugins = [];
|
||
|
const head = {
|
||
|
plugins,
|
||
|
dirty: false,
|
||
|
resolvedOptions: options,
|
||
|
hooks,
|
||
|
headEntries() {
|
||
|
return entries;
|
||
|
},
|
||
|
use(p) {
|
||
|
const plugin = typeof p === "function" ? p(head) : p;
|
||
|
if (!plugin.key || !plugins.some((p2) => p2.key === plugin.key)) {
|
||
|
plugins.push(plugin);
|
||
|
filterMode(plugin.mode, ssr) && hooks.addHooks(plugin.hooks || {});
|
||
|
}
|
||
|
},
|
||
|
push(input, entryOptions) {
|
||
|
delete entryOptions?.head;
|
||
|
const entry = {
|
||
|
_i: entryCount++,
|
||
|
input,
|
||
|
...entryOptions
|
||
|
};
|
||
|
if (filterMode(entry.mode, ssr)) {
|
||
|
entries.push(entry);
|
||
|
updated();
|
||
|
}
|
||
|
return {
|
||
|
dispose() {
|
||
|
entries = entries.filter((e) => e._i !== entry._i);
|
||
|
hooks.callHook("entries:updated", head);
|
||
|
updated();
|
||
|
},
|
||
|
// a patch is the same as creating a new entry, just a nice DX
|
||
|
patch(input2) {
|
||
|
entries = entries.map((e) => {
|
||
|
if (e._i === entry._i) {
|
||
|
e.input = entry.input = input2;
|
||
|
}
|
||
|
return e;
|
||
|
});
|
||
|
updated();
|
||
|
}
|
||
|
};
|
||
|
},
|
||
|
async resolveTags() {
|
||
|
const resolveCtx = { tags: [], entries: [...entries] };
|
||
|
await hooks.callHook("entries:resolve", resolveCtx);
|
||
|
for (const entry of resolveCtx.entries) {
|
||
|
const resolved = entry.resolvedInput || entry.input;
|
||
|
entry.resolvedInput = await (entry.transform ? entry.transform(resolved) : resolved);
|
||
|
if (entry.resolvedInput) {
|
||
|
for (const tag of await normaliseEntryTags(entry)) {
|
||
|
const tagCtx = { tag, entry, resolvedOptions: head.resolvedOptions };
|
||
|
await hooks.callHook("tag:normalise", tagCtx);
|
||
|
resolveCtx.tags.push(tagCtx.tag);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
await hooks.callHook("tags:beforeResolve", resolveCtx);
|
||
|
await hooks.callHook("tags:resolve", resolveCtx);
|
||
|
await hooks.callHook("tags:afterResolve", resolveCtx);
|
||
|
return resolveCtx.tags;
|
||
|
},
|
||
|
ssr
|
||
|
};
|
||
|
[
|
||
|
DedupePlugin,
|
||
|
PayloadPlugin,
|
||
|
EventHandlersPlugin,
|
||
|
HashKeyedPlugin,
|
||
|
SortPlugin,
|
||
|
TemplateParamsPlugin,
|
||
|
TitleTemplatePlugin,
|
||
|
XSSPlugin,
|
||
|
...options?.plugins || []
|
||
|
].forEach((p) => head.use(p));
|
||
|
head.hooks.callHook("init", head);
|
||
|
return head;
|
||
|
}
|
||
|
|
||
|
// @__NO_SIDE_EFFECTS__
|
||
|
function HashHydrationPlugin() {
|
||
|
return defineHeadPlugin({});
|
||
|
}
|
||
|
|
||
|
const importRe = /@import/;
|
||
|
// @__NO_SIDE_EFFECTS__
|
||
|
function CapoPlugin(options) {
|
||
|
return defineHeadPlugin({
|
||
|
hooks: {
|
||
|
"tags:beforeResolve": function({ tags }) {
|
||
|
for (const tag of tags) {
|
||
|
if (tag.tagPosition && tag.tagPosition !== "head")
|
||
|
continue;
|
||
|
tag.tagPriority = tag.tagPriority || tagWeight(tag);
|
||
|
if (tag.tagPriority !== 100)
|
||
|
continue;
|
||
|
const isTruthy = (val) => val === "" || val === true;
|
||
|
const isScript = tag.tag === "script";
|
||
|
const isLink = tag.tag === "link";
|
||
|
if (isScript && isTruthy(tag.props.async)) {
|
||
|
tag.tagPriority = 30;
|
||
|
} else if (tag.tag === "style" && tag.innerHTML && importRe.test(tag.innerHTML)) {
|
||
|
tag.tagPriority = 40;
|
||
|
} else if (isScript && tag.props.src && !isTruthy(tag.props.defer) && !isTruthy(tag.props.async) && tag.props.type !== "module" && !tag.props.type?.endsWith("json")) {
|
||
|
tag.tagPriority = 50;
|
||
|
} else if (isLink && tag.props.rel === "stylesheet" || tag.tag === "style") {
|
||
|
tag.tagPriority = 60;
|
||
|
} else if (isLink && ["preload", "modulepreload"].includes(tag.props.rel)) {
|
||
|
tag.tagPriority = 70;
|
||
|
} else if (isScript && isTruthy(tag.props.defer) && tag.props.src && !isTruthy(tag.props.async)) {
|
||
|
tag.tagPriority = 80;
|
||
|
} else if (isLink && ["prefetch", "dns-prefetch", "prerender"].includes(tag.props.rel)) {
|
||
|
tag.tagPriority = 90;
|
||
|
}
|
||
|
}
|
||
|
options?.track && tags.push({
|
||
|
tag: "htmlAttrs",
|
||
|
props: {
|
||
|
"data-capo": ""
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
const unheadComposablesImports = [
|
||
|
{
|
||
|
from: "unhead",
|
||
|
imports: composableNames
|
||
|
}
|
||
|
];
|
||
|
|
||
|
function getActiveHead() {
|
||
|
return activeHead;
|
||
|
}
|
||
|
|
||
|
function useHead(input, options = {}) {
|
||
|
const head = options.head || getActiveHead();
|
||
|
return head?.push(input, options);
|
||
|
}
|
||
|
|
||
|
function useHeadSafe(input, options = {}) {
|
||
|
return useHead(input, {
|
||
|
...options || {},
|
||
|
transform: whitelistSafeInput
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function useServerHead(input, options = {}) {
|
||
|
return useHead(input, { ...options, mode: "server" });
|
||
|
}
|
||
|
|
||
|
function useServerHeadSafe(input, options = {}) {
|
||
|
return useHeadSafe(input, { ...options, mode: "server" });
|
||
|
}
|
||
|
|
||
|
function useSeoMeta(input, options) {
|
||
|
const { title, titleTemplate, ...meta } = input;
|
||
|
return useHead({
|
||
|
title,
|
||
|
titleTemplate,
|
||
|
// we need to input the meta so the reactivity will be resolved
|
||
|
// @ts-expect-error runtime type
|
||
|
_flatMeta: meta
|
||
|
}, {
|
||
|
...options,
|
||
|
transform(t) {
|
||
|
const meta2 = unpackMeta({ ...t._flatMeta });
|
||
|
delete t._flatMeta;
|
||
|
return {
|
||
|
// @ts-expect-error runtime type
|
||
|
...t,
|
||
|
meta: meta2
|
||
|
};
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function useServerSeoMeta(input, options) {
|
||
|
return useSeoMeta(input, {
|
||
|
...options || {},
|
||
|
mode: "server"
|
||
|
});
|
||
|
}
|
||
|
|
||
|
const UseScriptDefaults = {
|
||
|
defer: true,
|
||
|
fetchpriority: "low"
|
||
|
};
|
||
|
function useScript(input, _options) {
|
||
|
const options = _options || {};
|
||
|
const head = options.head || getActiveHead();
|
||
|
if (!head)
|
||
|
throw new Error("No active head found, please provide a head instance or use the useHead composable");
|
||
|
const id = input.key || hashCode(input.src || (typeof input.innerHTML === "string" ? input.innerHTML : ""));
|
||
|
const key = `use-script.${id}`;
|
||
|
if (head._scripts?.[id])
|
||
|
return head._scripts[id];
|
||
|
async function transform(entry) {
|
||
|
const script2 = await (options.transform || ((input2) => input2))(entry.script[0]);
|
||
|
const ctx = { script: script2 };
|
||
|
await head.hooks.callHook("script:transform", ctx);
|
||
|
return { script: [ctx.script] };
|
||
|
}
|
||
|
function maybeHintEarlyConnection(rel) {
|
||
|
if (
|
||
|
// opt-out
|
||
|
options.skipEarlyConnections || !input.src.includes("//") || !head.ssr
|
||
|
)
|
||
|
return;
|
||
|
const key2 = `use-script.${id}.early-connection`;
|
||
|
head.push({
|
||
|
link: [{ key: key2, rel, href: new URL(input.src).origin }]
|
||
|
}, { mode: "server" });
|
||
|
}
|
||
|
const script = {
|
||
|
id,
|
||
|
status: "awaitingLoad",
|
||
|
loaded: false,
|
||
|
remove() {
|
||
|
if (script.status === "loaded") {
|
||
|
script.entry?.dispose();
|
||
|
script.status = "removed";
|
||
|
head.hooks.callHook(`script:updated`, hookCtx);
|
||
|
delete head._scripts?.[id];
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
},
|
||
|
waitForLoad() {
|
||
|
return new Promise((resolve) => {
|
||
|
if (script.status === "loaded")
|
||
|
resolve(options.use());
|
||
|
function watchForScriptLoaded({ script: script2 }) {
|
||
|
if (script2.id === id && script2.status === "loaded") {
|
||
|
resolve(options.use?.());
|
||
|
head.hooks.removeHook("script:updated", watchForScriptLoaded);
|
||
|
}
|
||
|
}
|
||
|
head.hooks.hook("script:updated", watchForScriptLoaded);
|
||
|
});
|
||
|
},
|
||
|
load() {
|
||
|
if (script.status !== "awaitingLoad")
|
||
|
return script.waitForLoad();
|
||
|
script.status = "loading";
|
||
|
head.hooks.callHook(`script:updated`, hookCtx);
|
||
|
script.entry = head.push({
|
||
|
script: [
|
||
|
// async by default
|
||
|
{ ...UseScriptDefaults, ...input, key }
|
||
|
]
|
||
|
}, {
|
||
|
...options,
|
||
|
// @ts-expect-error untyped
|
||
|
transform,
|
||
|
head
|
||
|
});
|
||
|
return script.waitForLoad();
|
||
|
}
|
||
|
};
|
||
|
const hookCtx = { script };
|
||
|
NetworkEvents.forEach((fn) => {
|
||
|
const _fn = typeof input[fn] === "function" ? input[fn].bind({}) : null;
|
||
|
input[fn] = (e) => {
|
||
|
script.status = fn === "onload" ? "loaded" : fn === "onerror" ? "error" : "loading";
|
||
|
head.hooks.callHook(`script:updated`, hookCtx);
|
||
|
_fn && _fn(e);
|
||
|
};
|
||
|
});
|
||
|
let trigger = options.trigger;
|
||
|
if (trigger) {
|
||
|
const isIdle = trigger === "idle";
|
||
|
if (isIdle) {
|
||
|
if (head.ssr)
|
||
|
trigger = "manual";
|
||
|
else
|
||
|
trigger = new Promise((resolve) => requestIdleCallback(() => resolve()));
|
||
|
}
|
||
|
trigger === "manual" && (trigger = new Promise(() => {
|
||
|
}));
|
||
|
trigger instanceof Promise && trigger.then(script.load);
|
||
|
maybeHintEarlyConnection(isIdle ? "preconnect" : "dns-prefetch");
|
||
|
} else {
|
||
|
script.load();
|
||
|
maybeHintEarlyConnection("preconnect");
|
||
|
}
|
||
|
function resolveInnerHtmlLoad(ctx) {
|
||
|
if (ctx.tag.key === key) {
|
||
|
if (ctx.tag.innerHTML) {
|
||
|
setTimeout(
|
||
|
() => {
|
||
|
script.status = "loaded";
|
||
|
head.hooks.callHook("script:updated", hookCtx);
|
||
|
typeof input.onload === "function" && input.onload(new Event("load"));
|
||
|
},
|
||
|
5
|
||
|
/* give inline script a chance to run */
|
||
|
);
|
||
|
}
|
||
|
head.hooks.removeHook("dom:renderTag", resolveInnerHtmlLoad);
|
||
|
}
|
||
|
}
|
||
|
head.hooks.hook("dom:renderTag", resolveInnerHtmlLoad);
|
||
|
const instance = new Proxy({}, {
|
||
|
get(_, fn) {
|
||
|
const stub = options.stub?.({ script, fn });
|
||
|
if (stub)
|
||
|
return stub;
|
||
|
if (fn === "$script")
|
||
|
return script;
|
||
|
return (...args) => {
|
||
|
if (head.ssr || !options.use)
|
||
|
return;
|
||
|
if (script.loaded) {
|
||
|
const api = options.use();
|
||
|
return api[fn](...args);
|
||
|
} else {
|
||
|
return script.waitForLoad().then(
|
||
|
(api) => {
|
||
|
api[fn](...args);
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
});
|
||
|
head._scripts = head._scripts || {};
|
||
|
head._scripts[id] = instance;
|
||
|
return instance;
|
||
|
}
|
||
|
|
||
|
export { CapoPlugin, HashHydrationPlugin, createHead, createHeadCore, createServerHead, getActiveHead, unheadComposablesImports, useHead, useHeadSafe, useScript, useSeoMeta, useServerHead, useServerHeadSafe, useServerSeoMeta };
|