From 334acb65d4bcaa5922db5d5241f174660ef18180 Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Mon, 2 Nov 2020 17:29:00 +0000 Subject: [PATCH] Revert "Remove `next-head-count` (#16758)" This reverts commit 039eb817e1bd04a99c070ac08bcf65937faeee66. --- errors/next-head-count-missing.md | 4 +- packages/next/client/head-manager.ts | 114 +++++++++--------- packages/next/client/index.tsx | 3 +- packages/next/next-server/lib/utils.ts | 3 - packages/next/next-server/server/render.tsx | 16 --- packages/next/pages/_document.tsx | 4 + .../build-output/test/index.test.js | 2 +- 7 files changed, 65 insertions(+), 81 deletions(-) diff --git a/errors/next-head-count-missing.md b/errors/next-head-count-missing.md index 6091c41d7a644..40c040f319461 100644 --- a/errors/next-head-count-missing.md +++ b/errors/next-head-count-missing.md @@ -6,8 +6,6 @@ You have a custom `pages/_document.js` that doesn't have the components required #### Possible Ways to Fix It -Upgrade Next.js to 9.5.4 or later, which does not require `next-head-count`. - -If you can't upgrade right now, ensure that your `_document.js` is importing and rendering all of the [required components](https://nextjs.org/docs/advanced-features/custom-document). +Ensure that your `_document.js` is importing and rendering all of the [required components](https://nextjs.org/docs/advanced-features/custom-document). In this case you are most likely not rendering the `` component imported from `next/document`. diff --git a/packages/next/client/head-manager.ts b/packages/next/client/head-manager.ts index 46f258e8c12c6..fc3689b303c2c 100644 --- a/packages/next/client/head-manager.ts +++ b/packages/next/client/head-manager.ts @@ -1,6 +1,3 @@ -import { createElement } from 'react' -import { HeadEntry } from '../next-server/lib/utils' - const DOMAttributeNames: Record = { acceptCharset: 'accept-charset', className: 'class', @@ -35,68 +32,51 @@ function reactElementToDOM({ type, props }: JSX.Element): HTMLElement { return el } -function updateElements( - elements: Set, - components: JSX.Element[], - removeOldTags: boolean -) { +function updateElements(type: string, components: JSX.Element[]) { const headEl = document.getElementsByTagName('head')[0] - const oldTags = new Set(elements) - - components.forEach((tag) => { - if (tag.type === 'title') { - let title = '' - if (tag) { - const { children } = tag.props - title = - typeof children === 'string' - ? children - : Array.isArray(children) - ? children.join('') - : '' - } - if (title !== document.title) document.title = title + const headCountEl: HTMLMetaElement = headEl.querySelector( + 'meta[name=next-head-count]' + ) as HTMLMetaElement + if (process.env.NODE_ENV !== 'production') { + if (!headCountEl) { + console.error( + 'Warning: next-head-count is missing. https://err.sh/next.js/next-head-count-missing' + ) return } + } - const newTag = reactElementToDOM(tag) - const elementIter = elements.values() - - while (true) { - // Note: We don't use for-of here to avoid needing to polyfill it. - const { done, value } = elementIter.next() - if (value?.isEqualNode(newTag)) { - oldTags.delete(value) - return - } + const headCount = Number(headCountEl.content) + const oldTags: Element[] = [] - if (done) { - break + for ( + let i = 0, j = headCountEl.previousElementSibling; + i < headCount; + i++, j = j!.previousElementSibling + ) { + if (j!.tagName.toLowerCase() === type) { + oldTags.push(j!) + } + } + const newTags = (components.map(reactElementToDOM) as HTMLElement[]).filter( + (newTag) => { + for (let k = 0, len = oldTags.length; k < len; k++) { + const oldTag = oldTags[k] + if (oldTag.isEqualNode(newTag)) { + oldTags.splice(k, 1) + return false + } } + return true } + ) - elements.add(newTag) - headEl.appendChild(newTag) - }) - - oldTags.forEach((oldTag) => { - if (removeOldTags) { - oldTag.parentNode!.removeChild(oldTag) - } - elements.delete(oldTag) - }) + oldTags.forEach((t) => t.parentNode!.removeChild(t)) + newTags.forEach((t) => headEl.insertBefore(t, headCountEl)) + headCountEl.content = (headCount - oldTags.length + newTags.length).toString() } -export default function initHeadManager(initialHeadEntries: HeadEntry[]) { - const headEl = document.getElementsByTagName('head')[0] - const elements = new Set(headEl.children) - - updateElements( - elements, - initialHeadEntries.map(([type, props]) => createElement(type, props)), - false - ) - +export default function initHeadManager() { let updatePromise: Promise | null = null return { @@ -106,7 +86,29 @@ export default function initHeadManager(initialHeadEntries: HeadEntry[]) { if (promise !== updatePromise) return updatePromise = null - updateElements(elements, head, true) + const tags: Record = {} + + head.forEach((h) => { + const components = tags[h.type] || [] + components.push(h) + tags[h.type] = components + }) + + const titleComponent = tags.title ? tags.title[0] : null + let title = '' + if (titleComponent) { + const { children } = titleComponent.props + title = + typeof children === 'string' + ? children + : Array.isArray(children) + ? children.join('') + : '' + } + if (title !== document.title) document.title = title + ;['meta', 'base', 'link', 'style', 'script'].forEach((type) => { + updateElements(type, tags[type] || []) + }) })) }, } diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 54f178e673b39..e0edd3bb2cf29 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -59,7 +59,6 @@ const { runtimeConfig, dynamicIds, isFallback, - head: initialHeadData, locales, } = data @@ -130,7 +129,7 @@ if (window.__NEXT_P) { window.__NEXT_P = [] ;(window.__NEXT_P as any).push = register -const headManager = initHeadManager(initialHeadData) +const headManager = initHeadManager() const appElement = document.getElementById('__next') let lastAppProps: AppProps diff --git a/packages/next/next-server/lib/utils.ts b/packages/next/next-server/lib/utils.ts index 057188def881c..5e9be12b7e5f6 100644 --- a/packages/next/next-server/lib/utils.ts +++ b/packages/next/next-server/lib/utils.ts @@ -81,8 +81,6 @@ export type BaseContext = { [k: string]: any } -export type HeadEntry = [string, { [key: string]: any }] - export type NEXT_DATA = { props: Record page: string @@ -100,7 +98,6 @@ export type NEXT_DATA = { customServer?: boolean gip?: boolean appGip?: boolean - head: HeadEntry[] locale?: string locales?: string[] defaultLocale?: string diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index d497c70036ac4..af750dba23de4 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -257,22 +257,6 @@ function renderDocument( locale, locales, defaultLocale, - head: React.Children.toArray(docProps.head || []) - .map((elem) => { - const { children } = elem?.props - return [ - elem?.type, - { - ...elem?.props, - children: children - ? typeof children === 'string' - ? children - : children.join('') - : undefined, - }, - ] - }) - .filter(Boolean) as any, }, buildManifest, docComponentsRendered, diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index 0b129671e54a4..ab36dae7558df 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -441,6 +441,10 @@ export class Head extends Component< )} {children} {head} + {inAmpMode && ( <> { expect(parseFloat(webpackSize) - 752).toBeLessThanOrEqual(0) expect(webpackSize.endsWith(' B')).toBe(true) - expect(parseFloat(mainSize) - 7.44).toBeLessThanOrEqual(0) + expect(parseFloat(mainSize) - 7.1).toBeLessThanOrEqual(0) expect(mainSize.endsWith('kB')).toBe(true) expect(parseFloat(frameworkSize) - 41).toBeLessThanOrEqual(0)