From 304824767c8afc6fca823f876232ca2517559df5 Mon Sep 17 00:00:00 2001 From: harlan Date: Wed, 8 Jan 2025 00:57:05 +1100 Subject: [PATCH] fix!: drop promise input support --- docs/content/_code-examples.md | 3 +- packages/shared/src/index.ts | 3 - packages/shared/src/normalise.ts | 90 ++++--------------- packages/shared/src/thenable.ts | 9 -- packages/unhead/build.config.ts | 1 + packages/unhead/src/optionalPlugins/index.ts | 1 + .../unhead/src/optionalPlugins/promises.ts | 37 ++++++++ packages/vue/test/promises.test.ts | 3 + packages/vue/test/util.ts | 5 +- test/unhead/promises.test.ts | 5 +- 10 files changed, 65 insertions(+), 92 deletions(-) delete mode 100644 packages/shared/src/thenable.ts create mode 100644 packages/unhead/src/optionalPlugins/index.ts create mode 100644 packages/unhead/src/optionalPlugins/promises.ts diff --git a/docs/content/_code-examples.md b/docs/content/_code-examples.md index 414d7210..6bd27f4f 100644 --- a/docs/content/_code-examples.md +++ b/docs/content/_code-examples.md @@ -20,8 +20,7 @@ useHead({ useServerHead({ link: [ { - // promises supported - href: import('~/assets/MyFont.css?url'), + href: '/assets/MyFont.css', rel: 'stylesheet', type: 'text/css' } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 87b8557c..9fc0bb8b 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -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' diff --git a/packages/shared/src/normalise.ts b/packages/shared/src/normalise.ts index e5546e07..9a8f1f98 100644 --- a/packages/shared/src/normalise.ts +++ b/packages/shared/src/normalise.ts @@ -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(tagName: T['tag'], input: HeadTag['props'] | string, e: HeadEntry, normalizedProps?: HeadTag['props']): Thenable { +export function normaliseTag(tagName: T['tag'], input: HeadTag['props'] | string, e: HeadEntry, normalizedProps?: HeadTag['props']): T | T[] { const props = normalizedProps || normaliseProps( // 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, @@ -68,14 +61,8 @@ export function normaliseStyleClassProps(key: T, v: .join(sep) } -function nestedNormaliseProps( - props: T['props'], - virtual: boolean, - keys: (keyof T['props'])[], - startIndex: number, -): Thenable { - for (let i = startIndex; i < keys.length; i += 1) { - const k = keys[i] +export function normaliseProps(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') { @@ -84,15 +71,6 @@ function nestedNormaliseProps( 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 @@ -110,44 +88,14 @@ function nestedNormaliseProps( } } } -} - -export function normaliseProps(props: T['props'], virtual: boolean = false): Thenable { - 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[], startIndex: number): Thenable { - 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(e: HeadEntry): Thenable { - const tagPromises: Thenable[] = [] +export function normaliseEntryTags(e: HeadEntry): HeadTag[] { + const tags: (HeadTag | HeadTag[])[] = [] const input = e.resolvedInput as T for (const k in input) { if (!Object.prototype.hasOwnProperty.call(input, k)) { @@ -160,26 +108,18 @@ export function normaliseEntryTags(e: HeadEntry): 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 + }) } diff --git a/packages/shared/src/thenable.ts b/packages/shared/src/thenable.ts deleted file mode 100644 index 6cc8e658..00000000 --- a/packages/shared/src/thenable.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type Thenable = Promise | T - -export function thenable(val: T, thenFn: (val: Awaited) => R): Promise | R { - if (val instanceof Promise) { - return val.then(thenFn) - } - - return thenFn(val as Awaited) -} diff --git a/packages/unhead/build.config.ts b/packages/unhead/build.config.ts index ec3c4bfc..af309168 100644 --- a/packages/unhead/build.config.ts +++ b/packages/unhead/build.config.ts @@ -8,5 +8,6 @@ export default defineBuildConfig({ }, entries: [ { input: 'src/index', name: 'index' }, + { input: 'src/optionalPlugins/index', name: 'optionalPlugins' }, ], }) diff --git a/packages/unhead/src/optionalPlugins/index.ts b/packages/unhead/src/optionalPlugins/index.ts new file mode 100644 index 00000000..392681d0 --- /dev/null +++ b/packages/unhead/src/optionalPlugins/index.ts @@ -0,0 +1 @@ +export * from './promises' diff --git a/packages/unhead/src/optionalPlugins/promises.ts b/packages/unhead/src/optionalPlugins/promises.ts new file mode 100644 index 00000000..68e7d064 --- /dev/null +++ b/packages/unhead/src/optionalPlugins/promises.ts @@ -0,0 +1,37 @@ +import { defineHeadPlugin } from '@unhead/shared' + +async function resolvePromisesRecursively(root: any): Promise { + 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 = {} + + 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) + } + }, + }, +})) diff --git a/packages/vue/test/promises.test.ts b/packages/vue/test/promises.test.ts index 621f2515..211e44dc 100644 --- a/packages/vue/test/promises.test.ts +++ b/packages/vue/test/promises.test.ts @@ -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', () => { @@ -14,6 +15,8 @@ describe('vue promises', () => { }, ], }) + }, { + plugins: [PromisesPlugin], }) expect(await head.resolveTags()).toMatchInlineSnapshot(` diff --git a/packages/vue/test/util.ts b/packages/vue/test/util.ts index 315f9994..4ab24271 100644 --- a/packages/vue/test/util.ts +++ b/packages/vue/test/util.ts @@ -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' @@ -28,8 +29,8 @@ export function csrVueAppWithUnhead(dom: JSDOM, fn: () => void | Promise) return head } -export async function ssrVueAppWithUnhead(fn: () => void | Promise) { - const head = createServerHead() +export async function ssrVueAppWithUnhead(fn: () => void | Promise, options?: CreateHeadOptions) { + const head = createServerHead(options) const app = createSSRApp({ async setup() { fn() diff --git a/test/unhead/promises.test.ts b/test/unhead/promises.test.ts index 77026901..0220f10e 100644 --- a/test/unhead/promises.test.ts +++ b/test/unhead/promises.test.ts @@ -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: [