fix(vue): eslint/tsconfig migration + resolve type errors
@robonen/vue (toolkit): migrate to eslint flat config + composite tsconfig; fix composable + test type errors (writable computed returns, null guards, overload-compatible signatures, typed test helpers) — all type-level.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { toRef, watch } from 'vue';
|
||||
import type { ComputedRef, MaybeRef, MaybeRefOrGetter, Ref } from 'vue';
|
||||
import { isString } from '@robonen/stdlib';
|
||||
import { defaultDocument } from '@/types';
|
||||
import type { ConfigurableDocument } from '@/types';
|
||||
|
||||
export interface UseFaviconOptions extends ConfigurableDocument {
|
||||
/**
|
||||
* Base URL prepended to the icon href
|
||||
*
|
||||
* @default ''
|
||||
*/
|
||||
baseUrl?: string;
|
||||
|
||||
/**
|
||||
* The `rel` attribute of the favicon link element
|
||||
*
|
||||
* @default 'icon'
|
||||
*/
|
||||
rel?: string;
|
||||
}
|
||||
|
||||
export type UseFaviconReturn = ComputedRef<string | null | undefined> | Ref<string | null | undefined>;
|
||||
|
||||
// Matches a real file extension at the very end of the path portion of the href,
|
||||
// e.g. `/foo.png` -> `png`, but NOT `/foo.png?v=2`, `data:...`, or extensionless hrefs.
|
||||
const FILE_EXTENSION_RE = /\.([a-z0-9]+)$/i;
|
||||
|
||||
/**
|
||||
* @name useFavicon
|
||||
* @category Browser
|
||||
* @description Reactive favicon.
|
||||
*
|
||||
* @param {MaybeRefOrGetter<string | null | undefined>} [newIcon] Initial icon href. A getter or readonly ref yields a read-only `ComputedRef`; a writable ref or plain value yields a writable `Ref`.
|
||||
* @param {UseFaviconOptions} [options={}] Options
|
||||
* @returns {UseFaviconReturn} A ref bound to the favicon href
|
||||
*
|
||||
* @example
|
||||
* const favicon = useFavicon();
|
||||
* favicon.value = '/new-icon.png';
|
||||
*
|
||||
* @example
|
||||
* // Track an existing reactive source (read-only result)
|
||||
* const isDark = useDark();
|
||||
* const favicon = useFavicon(() => isDark.value ? '/dark.png' : '/light.png');
|
||||
*
|
||||
* @since 0.0.15
|
||||
*/
|
||||
export function useFavicon(
|
||||
newIcon: MaybeRefOrGetter<string | null | undefined>,
|
||||
options?: UseFaviconOptions,
|
||||
): ComputedRef<string | null | undefined>;
|
||||
export function useFavicon(
|
||||
newIcon?: MaybeRef<string | null | undefined>,
|
||||
options?: UseFaviconOptions,
|
||||
): Ref<string | null | undefined>;
|
||||
export function useFavicon(
|
||||
newIcon: MaybeRefOrGetter<string | null | undefined> = null,
|
||||
options: UseFaviconOptions = {},
|
||||
): UseFaviconReturn {
|
||||
const {
|
||||
baseUrl = '',
|
||||
rel = 'icon',
|
||||
document = defaultDocument,
|
||||
} = options;
|
||||
|
||||
const favicon = toRef(newIcon);
|
||||
const selector = `link[rel*="${rel}"]`;
|
||||
|
||||
const applyIcon = (icon: string) => {
|
||||
if (!document)
|
||||
return;
|
||||
|
||||
const href = `${baseUrl}${icon}`;
|
||||
const elements = document.head.querySelectorAll<HTMLLinkElement>(selector);
|
||||
|
||||
if (!elements.length) {
|
||||
const link = document.createElement('link');
|
||||
|
||||
link.rel = rel;
|
||||
link.href = href;
|
||||
|
||||
// Only set a MIME type when the icon actually ends in a file extension;
|
||||
// otherwise we'd emit garbage like `image/png?v=2` or `image/` for
|
||||
// query-string, extensionless, or data: hrefs.
|
||||
const extension = FILE_EXTENSION_RE.exec(icon)?.[1];
|
||||
if (extension)
|
||||
link.type = `image/${extension}`;
|
||||
|
||||
document.head.append(link);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const element of elements)
|
||||
element.href = href;
|
||||
};
|
||||
|
||||
watch(
|
||||
favicon,
|
||||
(icon, oldIcon) => {
|
||||
if (isString(icon) && icon !== oldIcon)
|
||||
applyIcon(icon);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return favicon;
|
||||
}
|
||||
Reference in New Issue
Block a user