Skip to content

Commit

Permalink
Revert "Remove next-head-count (vercel#16758)"
Browse files Browse the repository at this point in the history
This reverts commit 039eb81.
  • Loading branch information
devknoll committed Nov 2, 2020
1 parent a529e67 commit 334acb6
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 81 deletions.
4 changes: 1 addition & 3 deletions errors/next-head-count-missing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Head>` component imported from `next/document`.
114 changes: 58 additions & 56 deletions packages/next/client/head-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { createElement } from 'react'
import { HeadEntry } from '../next-server/lib/utils'

const DOMAttributeNames: Record<string, string> = {
acceptCharset: 'accept-charset',
className: 'class',
Expand Down Expand Up @@ -35,68 +32,51 @@ function reactElementToDOM({ type, props }: JSX.Element): HTMLElement {
return el
}

function updateElements(
elements: Set<Element>,
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<Element>(headEl.children)

updateElements(
elements,
initialHeadEntries.map(([type, props]) => createElement(type, props)),
false
)

export default function initHeadManager() {
let updatePromise: Promise<void> | null = null

return {
Expand All @@ -106,7 +86,29 @@ export default function initHeadManager(initialHeadEntries: HeadEntry[]) {
if (promise !== updatePromise) return

updatePromise = null
updateElements(elements, head, true)
const tags: Record<string, JSX.Element[]> = {}

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] || [])
})
}))
},
}
Expand Down
3 changes: 1 addition & 2 deletions packages/next/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ const {
runtimeConfig,
dynamicIds,
isFallback,
head: initialHeadData,
locales,
} = data

Expand Down Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions packages/next/next-server/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ export type BaseContext = {
[k: string]: any
}

export type HeadEntry = [string, { [key: string]: any }]

export type NEXT_DATA = {
props: Record<string, any>
page: string
Expand All @@ -100,7 +98,6 @@ export type NEXT_DATA = {
customServer?: boolean
gip?: boolean
appGip?: boolean
head: HeadEntry[]
locale?: string
locales?: string[]
defaultLocale?: string
Expand Down
16 changes: 0 additions & 16 deletions packages/next/next-server/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions packages/next/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,10 @@ export class Head extends Component<
)}
{children}
{head}
<meta
name="next-head-count"
content={React.Children.count(head || []).toString()}
/>
{inAmpMode && (
<>
<meta
Expand Down
2 changes: 1 addition & 1 deletion test/integration/build-output/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe('Build Output', () => {
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)
Expand Down

0 comments on commit 334acb6

Please sign in to comment.