PC-mj/.output/server/node_modules/unhead/dist/index.mjs
2024-01-03 10:32:21 +08:00

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 };