Skip to content

Commit

Permalink
fix!: drop promise input support
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw committed Jan 7, 2025
1 parent 889e61a commit 3048247
Show file tree
Hide file tree
Showing 10 changed files with 65 additions and 92 deletions.
3 changes: 1 addition & 2 deletions docs/content/_code-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ useHead({
useServerHead({
link: [
{
// promises supported
href: import('~/assets/MyFont.css?url'),
href: '/assets/MyFont.css',
rel: 'stylesheet',
type: 'text/css'
}
Expand Down
3 changes: 0 additions & 3 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
export * from './array'
export * from './constants'
export * from './defineHeadPlugin'
export * from './hashCode'
export * from './meta'
export * from './normalise'
export * from './safe'
export * from './script'
export * from './sort'
export * from './tagDedupeKey'
export * from './templateParams'
export * from './thenable'
export * from './titleTemplate'
90 changes: 15 additions & 75 deletions packages/shared/src/normalise.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import type { Head, HeadEntry, HeadTag } from '@unhead/schema'
import { TagConfigKeys, TagsWithInnerContent, ValidHeadTags } from '.'
import { type Thenable, thenable } from './thenable'
import { TagConfigKeys, TagsWithInnerContent, ValidHeadTags } from './constants'

export function normaliseTag<T extends HeadTag>(tagName: T['tag'], input: HeadTag['props'] | string, e: HeadEntry<T>, normalizedProps?: HeadTag['props']): Thenable<T | T[]> {
export function normaliseTag<T extends HeadTag>(tagName: T['tag'], input: HeadTag['props'] | string, e: HeadEntry<T>, normalizedProps?: HeadTag['props']): T | T[] {
const props = normalizedProps || normaliseProps<T>(
// explicitly check for an object
// @ts-expect-error untyped
typeof input === 'object' && typeof input !== 'function' && !(input instanceof Promise)
typeof input === 'object' && typeof input !== 'function'
? { ...input }
: { [(tagName === 'script' || tagName === 'noscript' || tagName === 'style') ? 'innerHTML' : 'textContent']: input },
(tagName === 'templateParams' || tagName === 'titleTemplate'),
)

if (props instanceof Promise) {
return props.then(val => normaliseTag(tagName, input, e, val))
}

// input can be a function or an object, we need to clone it
const tag = {
tag: tagName,
Expand Down Expand Up @@ -68,14 +61,8 @@ export function normaliseStyleClassProps<T extends 'class' | 'style'>(key: T, v:
.join(sep)
}

function nestedNormaliseProps<T extends HeadTag>(
props: T['props'],
virtual: boolean,
keys: (keyof T['props'])[],
startIndex: number,
): Thenable<void> {
for (let i = startIndex; i < keys.length; i += 1) {
const k = keys[i]
export function normaliseProps<T extends HeadTag>(props: T['props'], virtual: boolean = false) {
for (const k in props) {
// handle boolean props, see https://html.spec.whatwg.org/#boolean-attributes
// class has special handling
if (k === 'class' || k === 'style') {
Expand All @@ -84,15 +71,6 @@ function nestedNormaliseProps<T extends HeadTag>(
continue
}

// @ts-expect-error no reason for: The left-hand side of an 'instanceof' expression must be of type 'any', an object type or a type parameter.
if (props[k] instanceof Promise) {
return props[k].then((val) => {
props[k] = val

return nestedNormaliseProps(props, virtual, keys, i)
})
}

if (!virtual && !TagConfigKeys.has(k as string)) {
const v = String(props[k])
// data keys get special treatment, we opt for more verbose syntax
Expand All @@ -110,44 +88,14 @@ function nestedNormaliseProps<T extends HeadTag>(
}
}
}
}

export function normaliseProps<T extends HeadTag>(props: T['props'], virtual: boolean = false): Thenable<T['props']> {
const resolvedProps = nestedNormaliseProps(props, virtual, Object.keys(props), 0)

if (resolvedProps instanceof Promise) {
return resolvedProps.then(() => props)
}

return props
}

// support 1024 tag ids per entry (includes updates)
export const TagEntityBits = 10

function nestedNormaliseEntryTags(headTags: HeadTag[], tagPromises: Thenable<HeadTag | HeadTag[]>[], startIndex: number): Thenable<unknown> {
for (let i = startIndex; i < tagPromises.length; i += 1) {
const tags = tagPromises[i]

if (tags instanceof Promise) {
return tags.then((val) => {
tagPromises[i] = val

return nestedNormaliseEntryTags(headTags, tagPromises, i)
})
}

if (Array.isArray(tags)) {
headTags.push(...tags)
}
else {
headTags.push(tags)
}
}
}

export function normaliseEntryTags<T extends object = Head>(e: HeadEntry<T>): Thenable<HeadTag[]> {
const tagPromises: Thenable<HeadTag | HeadTag[]>[] = []
export function normaliseEntryTags<T extends object = Head>(e: HeadEntry<T>): HeadTag[] {
const tags: (HeadTag | HeadTag[])[] = []
const input = e.resolvedInput as T
for (const k in input) {
if (!Object.prototype.hasOwnProperty.call(input, k)) {
Expand All @@ -160,26 +108,18 @@ export function normaliseEntryTags<T extends object = Head>(e: HeadEntry<T>): Th
if (Array.isArray(v)) {
for (const props of v) {
// @ts-expect-error untyped
tagPromises.push(normaliseTag(k as keyof Head, props, e))
tags.push(normaliseTag(k as keyof Head, props, e))
}
continue
}
// @ts-expect-error untyped
tagPromises.push(normaliseTag(k as keyof Head, v, e))
tags.push(normaliseTag(k as keyof Head, v, e))
}

if (tagPromises.length === 0) {
return []
}

const headTags: HeadTag[] = []

return thenable(nestedNormaliseEntryTags(headTags, tagPromises, 0), () => (
headTags.map((t, i) => {
t._e = e._i
e.mode && (t._m = e.mode)
t._p = (e._i << TagEntityBits) + i
return t
})
))
return tags.flat().map((t, i) => {
t._e = e._i
e.mode && (t._m = e.mode)
t._p = (e._i << TagEntityBits) + i
return t
})
}
9 changes: 0 additions & 9 deletions packages/shared/src/thenable.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/unhead/build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export default defineBuildConfig({
},
entries: [
{ input: 'src/index', name: 'index' },
{ input: 'src/optionalPlugins/index', name: 'optionalPlugins' },
],
})
1 change: 1 addition & 0 deletions packages/unhead/src/optionalPlugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './promises'
37 changes: 37 additions & 0 deletions packages/unhead/src/optionalPlugins/promises.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { defineHeadPlugin } from '@unhead/shared'

async function resolvePromisesRecursively(root: any): Promise<any> {
if (root instanceof Promise) {
return await root
}
// could be a root primitive, array or object
if (Array.isArray(root)) {
return Promise.all(root.map(r => resolvePromisesRecursively(r)))
}
if (typeof root === 'object') {
const resolved: Record<string, string> = {}

for (const k in root) {
if (!Object.prototype.hasOwnProperty.call(root, k)) {
continue
}

resolved[k] = await resolvePromisesRecursively(root[k])
}

return resolved
}
return root
}

export const PromisesPlugin = defineHeadPlugin(head => ({
hooks: {
'entries:resolve': async (ctx) => {
for (const k in ctx.entries) {
const resolved = await resolvePromisesRecursively(ctx.entries[k].input)
ctx.entries[k].input = resolved
console.log('resolved', ctx.entries[k].input, resolved)
}
},
},
}))
3 changes: 3 additions & 0 deletions packages/vue/test/promises.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useHead } from '@unhead/vue'
import { describe, it } from 'vitest'
import { PromisesPlugin } from '../../unhead/src/optionalPlugins/promises'
import { ssrVueAppWithUnhead } from './util'

describe('vue promises', () => {
Expand All @@ -14,6 +15,8 @@ describe('vue promises', () => {
},
],
})
}, {
plugins: [PromisesPlugin],
})

expect(await head.resolveTags()).toMatchInlineSnapshot(`
Expand Down
5 changes: 3 additions & 2 deletions packages/vue/test/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @vitest-environment jsdom

import type { CreateHeadOptions } from '@unhead/schema'
import type { JSDOM } from 'jsdom'
import type { App, Component } from 'vue'
import { renderSSRHead } from '@unhead/ssr'
Expand Down Expand Up @@ -28,8 +29,8 @@ export function csrVueAppWithUnhead(dom: JSDOM, fn: () => void | Promise<void>)
return head
}

export async function ssrVueAppWithUnhead(fn: () => void | Promise<void>) {
const head = createServerHead()
export async function ssrVueAppWithUnhead(fn: () => void | Promise<void>, options?: CreateHeadOptions) {
const head = createServerHead(options)
const app = createSSRApp({
async setup() {
fn()
Expand Down
5 changes: 4 additions & 1 deletion test/unhead/promises.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { describe, it } from 'vitest'
import { PromisesPlugin } from '../../packages/unhead/src/optionalPlugins/promises'
import { createHeadWithContext } from '../util'

describe('promises', () => {
it('basic', async () => {
const head = createHeadWithContext()
const head = createHeadWithContext({
plugins: [PromisesPlugin],
})
head.push({
title: new Promise(resolve => resolve('hello')),
script: [
Expand Down

0 comments on commit 3048247

Please sign in to comment.