Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: vanilla function resolves #443

Merged
merged 4 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions packages/schema/src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Base as _Base, HtmlAttributes as _HtmlAttributes, Meta as _Meta, Noscript as _Noscript, Style as _Style, BaseBodyAttributes, BodyEvents, DataKeys, DefinedValueOrEmptyObject, HttpEventAttributes, LinkBase, Merge, MergeHead, MetaFlatInput, ScriptBase, Stringable } from 'zhead'
import type { InnerContent, ResolvesDuplicates, TagPosition, TagPriority, TagUserProperties, TemplateParams } from './tags'
import type { FalsyEntries, Never } from './util'
import type { Falsey, MaybeFunction, Never, ResolvableValues } from './util'

export type UserTagConfigWithoutInnerContent = TagPriority & TagPosition & ResolvesDuplicates & Never<InnerContent> & { processTemplateParams?: false } // only allow opt-out
export type UserAttributesConfig = ResolvesDuplicates & TagPriority & Never<InnerContent & TagPosition>
Expand Down Expand Up @@ -54,22 +54,22 @@ export interface BaseMeta extends Omit<_Meta, 'content'> {

export type EntryAugmentation = undefined | Record<string, any>

export type MaybeFunctionEntries<T> = {
export type MaybeEventFnHandlers<T> = {
[key in keyof T]?: T[key] | ((e: Event) => void)
}

type TitleTemplateResolver = string | ((title?: string) => string | null)

export type Title = string | FalsyEntries<({ textContent: string } & SchemaAugmentations['title']) | null>
export type Title = MaybeFunction<number | string | Falsey> | ResolvableValues<({ textContent: string } & SchemaAugmentations['title'])>
export type TitleTemplate = TitleTemplateResolver | null | ({ textContent: TitleTemplateResolver } & SchemaAugmentations['titleTemplate'])
export type Base<E extends EntryAugmentation = Record<string, any>> = Partial<Merge<SchemaAugmentations['base'], FalsyEntries<_Base>>> & DefinedValueOrEmptyObject<E>
export type Link<E extends EntryAugmentation = Record<string, any>> = FalsyEntries<LinkBase> & MaybeFunctionEntries<HttpEventAttributes> & DataKeys & SchemaAugmentations['link'] & DefinedValueOrEmptyObject<E>
export type Meta<E extends EntryAugmentation = Record<string, any>> = FalsyEntries<BaseMeta> & DataKeys & SchemaAugmentations['meta'] & DefinedValueOrEmptyObject<E>
export type Style<E extends EntryAugmentation = Record<string, any>> = FalsyEntries<_Style> & DataKeys & SchemaAugmentations['style'] & DefinedValueOrEmptyObject<E>
export type Script<E extends EntryAugmentation = Record<string, any>> = FalsyEntries<ScriptBase> & MaybeFunctionEntries<HttpEventAttributes> & DataKeys & SchemaAugmentations['script'] & DefinedValueOrEmptyObject<E>
export type Noscript<E extends EntryAugmentation = Record<string, any>> = FalsyEntries<_Noscript> & DataKeys & SchemaAugmentations['noscript'] & DefinedValueOrEmptyObject<E>
export type HtmlAttributes<E extends EntryAugmentation = Record<string, any>> = FalsyEntries<HtmlAttr> & DataKeys & SchemaAugmentations['htmlAttrs'] & DefinedValueOrEmptyObject<E>
export type BodyAttributes<E extends EntryAugmentation = Record<string, any>> = FalsyEntries<BodyAttr> & MaybeFunctionEntries<BodyEvents> & DataKeys & SchemaAugmentations['bodyAttrs'] & DefinedValueOrEmptyObject<E>
export type Base<E extends EntryAugmentation = Record<string, any>> = Partial<Merge<SchemaAugmentations['base'], ResolvableValues<_Base>>> & DefinedValueOrEmptyObject<E>
export type Link<E extends EntryAugmentation = Record<string, any>> = ResolvableValues<LinkBase> & MaybeEventFnHandlers<HttpEventAttributes> & DataKeys & SchemaAugmentations['link'] & DefinedValueOrEmptyObject<E>
export type Meta<E extends EntryAugmentation = Record<string, any>> = ResolvableValues<BaseMeta> & DataKeys & SchemaAugmentations['meta'] & DefinedValueOrEmptyObject<E>
export type Style<E extends EntryAugmentation = Record<string, any>> = ResolvableValues<_Style> & DataKeys & SchemaAugmentations['style'] & DefinedValueOrEmptyObject<E>
export type Script<E extends EntryAugmentation = Record<string, any>> = ResolvableValues<ScriptBase> & MaybeEventFnHandlers<HttpEventAttributes> & DataKeys & SchemaAugmentations['script'] & DefinedValueOrEmptyObject<E>
export type Noscript<E extends EntryAugmentation = Record<string, any>> = ResolvableValues<_Noscript> & DataKeys & SchemaAugmentations['noscript'] & DefinedValueOrEmptyObject<E>
export type HtmlAttributes<E extends EntryAugmentation = Record<string, any>> = ResolvableValues<HtmlAttr> & DataKeys & SchemaAugmentations['htmlAttrs'] & DefinedValueOrEmptyObject<E>
export type BodyAttributes<E extends EntryAugmentation = Record<string, any>> = ResolvableValues<BodyAttr> & MaybeEventFnHandlers<BodyEvents> & DataKeys & SchemaAugmentations['bodyAttrs'] & DefinedValueOrEmptyObject<E>

export interface HeadUtils {
/**
Expand Down
4 changes: 2 additions & 2 deletions packages/schema/src/tags.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { RuntimeMode } from './head'
import type { Head } from './schema'
import type { FalsyEntries } from './util'
import type { ResolvableValues } from './util'

export interface ResolvesDuplicates {
/**
Expand Down Expand Up @@ -60,7 +60,7 @@ export interface TagPriority {
tagPriority?: number | 'critical' | 'high' | 'low' | `before:${string}` | `after:${string}`
}

export type TagUserProperties = FalsyEntries<TagPriority & TagPosition & InnerContent & ResolvesDuplicates & ProcessesTemplateParams>
export type TagUserProperties = ResolvableValues<TagPriority & TagPosition & InnerContent & ResolvesDuplicates & ProcessesTemplateParams>

export type TagKey = keyof Head

Expand Down
8 changes: 6 additions & 2 deletions packages/schema/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ export type Never<T> = {
[P in keyof T]?: never
}

export type FalsyEntries<T> = {
[key in keyof T]?: T[key] | null | false | undefined // false is soft deprecated
export type MaybeFunction<T> = T | (() => T)

export type Falsey = false | null | undefined

export type ResolvableValues<T> = {
[key in keyof T]?: MaybeFunction<T[key] | Falsey>
}
9 changes: 9 additions & 0 deletions packages/shared/src/normalise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export function normaliseProps<T extends HeadTag>(props: T['props'], virtual: bo
}

if (!virtual && !TagConfigKeys.has(k as string)) {
if (typeof props[k] === 'function' && !String(k).startsWith('on')) {
// @ts-expect-error untyped
props[k] = props[k]()
}
const v = String(props[k])
// data keys get special treatment, we opt for more verbose syntax
const isDataKey = (k as string).startsWith('data-')
Expand Down Expand Up @@ -105,6 +109,11 @@ export function normaliseEntryTags<T extends object = Head>(e: HeadEntry<T>): He
}
continue
}
else if (typeof v === 'function' && k !== 'titleTemplate') {
// resolve titles that may be functions
input[k] = v()
continue
}
// @ts-expect-error untyped
tags.push(normaliseTag(k as keyof Head, v, e))
}
Expand Down
1 change: 1 addition & 0 deletions packages/unhead/src/optionalPlugins/inferSeoMetaPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export function InferSeoMetaPlugin(options: InferSeoMetaPluginOptions = {}) {
if (description && !hasOgDescription) {
let newOgDescription = options?.ogDescription || description
if (typeof newOgDescription === 'function')
// @ts-expect-error untyped
newOgDescription = newOgDescription(title)

if (newOgDescription) {
Expand Down
4 changes: 2 additions & 2 deletions packages/vue/src/types/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Base as _Base, Link as _Link, Meta as _Meta, Noscript as _Noscript, Script as _Script, Style as _Style, Title as _Title, TitleTemplate as _TitleTemplate, BaseBodyAttr, BaseHtmlAttr, BodyEvents, DataKeys, DefinedValueOrEmptyObject, EntryAugmentation, HeadEntryOptions, MaybeArray, MaybeFunctionEntries, MergeHead, MetaFlatInput, SchemaAugmentations, Unhead } from '@unhead/schema'
import type { Base as _Base, Link as _Link, Meta as _Meta, Noscript as _Noscript, Script as _Script, Style as _Style, Title as _Title, TitleTemplate as _TitleTemplate, BaseBodyAttr, BaseHtmlAttr, BodyEvents, DataKeys, DefinedValueOrEmptyObject, EntryAugmentation, HeadEntryOptions, MaybeArray, MaybeEventFnHandlers, MergeHead, MetaFlatInput, SchemaAugmentations, Unhead } from '@unhead/schema'
import type { Plugin, Ref } from 'vue'
import type { MaybeComputedRef, MaybeComputedRefEntries, MaybeComputedRefEntriesOnly } from './util'

Expand Down Expand Up @@ -35,7 +35,7 @@ export type Style<E extends EntryAugmentation = Record<string, any>> = MaybeComp
export type Script<E extends EntryAugmentation = Record<string, any>> = MaybeComputedRefEntries<_Script<E>>
export type Noscript<E extends EntryAugmentation = Record<string, any>> = MaybeComputedRefEntries<_Noscript<E>>
export type HtmlAttributes<E extends EntryAugmentation = Record<string, any>> = MaybeComputedRef<MaybeComputedRefEntries<HtmlAttr & DataKeys & SchemaAugmentations['htmlAttrs'] & DefinedValueOrEmptyObject<E>>>
export type BodyAttributes<E extends EntryAugmentation = Record<string, any>> = MaybeComputedRef<MaybeComputedRefEntries<BodyAttr & DataKeys & SchemaAugmentations['bodyAttrs'] & DefinedValueOrEmptyObject<E>> & MaybeFunctionEntries<BodyEvents>>
export type BodyAttributes<E extends EntryAugmentation = Record<string, any>> = MaybeComputedRef<MaybeComputedRefEntries<BodyAttr & DataKeys & SchemaAugmentations['bodyAttrs'] & DefinedValueOrEmptyObject<E>> & MaybeEventFnHandlers<BodyEvents>>

export interface ReactiveHead<E extends MergeHead = MergeHead> {
/**
Expand Down
4 changes: 2 additions & 2 deletions packages/vue/src/types/util.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ComputedRef, Ref } from 'vue'

// copied from @vueuse/shared
export type MaybeReadonlyRef<T> = (() => T) | ComputedRef<T>
export type MaybeReadonlyRef<T> = ComputedRef<T>
export type MaybeComputedRef<T> = T | MaybeReadonlyRef<T> | Ref<T>
export type MaybeComputedRefOrFalsy<T> = undefined | false | null | T | MaybeReadonlyRef<T> | Ref<T>
export type MaybeComputedRefOrFalsy<T> = T | MaybeReadonlyRef<T> | Ref<T>

/**
* @deprecated Use MaybeComputedRefOrFalsy
Expand Down
28 changes: 26 additions & 2 deletions test/unhead/ssr/ssr.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { renderSSRHead } from '@unhead/ssr'
import { useSeoMeta } from 'unhead'
import { useHead, useSeoMeta } from 'unhead'
import { describe, it } from 'vitest'
import { basicSchema } from '../../fixtures'
import { createHeadWithContext } from '../../util'
Expand Down Expand Up @@ -33,7 +33,6 @@ describe('ssr', () => {
const head = createHeadWithContext()

head.push({
// @ts-expect-error handle numbers
title: 12345,
})

Expand Down Expand Up @@ -190,4 +189,29 @@ describe('ssr', () => {
}
`)
})

it('title function', async () => {
const head = createHeadWithContext()

useHead({
title: 'my default title',
})

useHead({
title: () => {
return undefined
},
})

const ctx = await renderSSRHead(head)
expect(ctx).toMatchInlineSnapshot(`
{
"bodyAttrs": "",
"bodyTags": "",
"bodyTagsOpen": "",
"headTags": "<title>my default title</title>",
"htmlAttrs": "",
}
`)
})
})
Loading