From 6ce3e15e87ec407b3cfbcb09ca318e6395c8398d Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 20 Oct 2022 11:55:08 -0700 Subject: [PATCH 1/4] Add initial head handling in app --- packages/next/server/app-render.tsx | 23 +++++ test/e2e/app-dir/head.test.ts | 95 +++++++++++++++++++ test/e2e/app-dir/head/app/blog/[slug]/page.js | 18 ++++ test/e2e/app-dir/head/app/blog/about/page.js | 7 ++ test/e2e/app-dir/head/app/blog/layout.js | 21 ++++ test/e2e/app-dir/head/app/blog/page.js | 18 ++++ test/e2e/app-dir/head/app/layout.js | 10 ++ test/e2e/app-dir/head/app/page.js | 19 ++++ test/e2e/app-dir/head/next.config.js | 17 ++++ test/e2e/app-dir/head/public/another.js | 4 + test/e2e/app-dir/head/public/hello.js | 4 + test/e2e/app-dir/head/public/hello1.js | 4 + test/e2e/app-dir/head/public/hello2.js | 4 + test/e2e/app-dir/head/public/hello3.js | 4 + 14 files changed, 248 insertions(+) create mode 100644 test/e2e/app-dir/head.test.ts create mode 100644 test/e2e/app-dir/head/app/blog/[slug]/page.js create mode 100644 test/e2e/app-dir/head/app/blog/about/page.js create mode 100644 test/e2e/app-dir/head/app/blog/layout.js create mode 100644 test/e2e/app-dir/head/app/blog/page.js create mode 100644 test/e2e/app-dir/head/app/layout.js create mode 100644 test/e2e/app-dir/head/app/page.js create mode 100644 test/e2e/app-dir/head/next.config.js create mode 100644 test/e2e/app-dir/head/public/another.js create mode 100644 test/e2e/app-dir/head/public/hello.js create mode 100644 test/e2e/app-dir/head/public/hello1.js create mode 100644 test/e2e/app-dir/head/public/hello2.js create mode 100644 test/e2e/app-dir/head/public/hello3.js diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index e12dd045c71c096..f5e51135b0b9607 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -914,12 +914,19 @@ export async function renderToHTMLOrFlight( parentParams, firstItem, rootLayoutIncluded, + collectedHeads = [], }: { createSegmentPath: CreateSegmentPath loaderTree: LoaderTree parentParams: { [key: string]: any } rootLayoutIncluded?: boolean firstItem?: boolean + collectedHeads?: Array< + (ctx: { + params?: Record + searchParams?: Record + }) => Promise + > }): Promise<{ Component: React.ComponentType }> => { // TODO-APP: enable stylesheet per layout/page const stylesheets: string[] = layoutOrPagePath @@ -1010,6 +1017,16 @@ export async function renderToHTMLOrFlight( // Resolve the segment param const actualSegment = segmentParam ? segmentParam.treeSegment : segment + // collect head pieces + if (typeof layoutOrPageMod?.Head === 'function') { + collectedHeads.push(() => + layoutOrPageMod.Head({ + params: currentParams, + ...(isPage ? { searchParams: query } : {}), + }) + ) + } + // This happens outside of rendering in order to eagerly kick off data fetching for layouts / the page further down const parallelRouteMap = await Promise.all( Object.keys(parallelRoutes).map( @@ -1059,6 +1076,7 @@ export async function renderToHTMLOrFlight( loaderTree: parallelRoutes[parallelRouteKey], parentParams: currentParams, rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove, + collectedHeads, }) const childProp: ChildProp = { @@ -1117,9 +1135,14 @@ export async function renderToHTMLOrFlight( // Add extra cache busting (DEV only) for https://github.com/vercel/next.js/issues/5860 // See also https://bugs.webkit.org/show_bug.cgi?id=187726 const cacheBustingUrlSuffix = dev ? `?ts=${Date.now()}` : '' + let HeadTags + if (rootLayoutAtThisLevel) { + HeadTags = collectedHeads[collectedHeads.length - 1] as any + } return ( <> + {HeadTags ? : null} {preloadedFontFiles.map((fontFile) => { const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(fontFile)![1] return ( diff --git a/test/e2e/app-dir/head.test.ts b/test/e2e/app-dir/head.test.ts new file mode 100644 index 000000000000000..b39f4f8493d80a6 --- /dev/null +++ b/test/e2e/app-dir/head.test.ts @@ -0,0 +1,95 @@ +import path from 'path' +import cheerio from 'cheerio' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { renderViaHTTP } from 'next-test-utils' + +describe('app dir head', () => { + if ((global as any).isNextDeploy) { + it('should skip next deploy for now', () => {}) + return + } + + if (process.env.NEXT_TEST_REACT_VERSION === '^17') { + it('should skip for react v17', () => {}) + return + } + let next: NextInstance + + function runTests() { + beforeAll(async () => { + next = await createNext({ + files: new FileRef(path.join(__dirname, 'head')), + dependencies: { + react: 'experimental', + 'react-dom': 'experimental', + }, + skipStart: true, + }) + + await next.start() + }) + afterAll(() => next.destroy()) + + it('should use head from index page', async () => { + const html = await renderViaHTTP(next.url, '/') + const $ = cheerio.load(html) + const headTags = $('head').children().toArray() + + expect(headTags.find((el) => el.attribs.src === '/hello.js')).toBeTruthy() + expect( + headTags.find((el) => el.attribs.src === '/another.js') + ).toBeTruthy() + }) + + it('should not use head from layout when on page', async () => { + const html = await renderViaHTTP(next.url, '/blog') + const $ = cheerio.load(html) + const headTags = $('head').children().toArray() + + expect( + headTags.find((el) => el.attribs.src === '/hello3.js') + ).toBeTruthy() + expect(headTags.find((el) => el.attribs.src === '/hello1.js')).toBeFalsy() + expect(headTags.find((el) => el.attribs.src === '/hello2.js')).toBeFalsy() + expect( + headTags.find((el) => el.attribs.src === '/another.js') + ).toBeTruthy() + }) + + it('should use head from layout when not on page', async () => { + const html = await renderViaHTTP(next.url, '/blog/about') + const $ = cheerio.load(html) + const headTags = $('head').children().toArray() + + expect( + headTags.find((el) => el.attribs.src === '/hello1.js') + ).toBeTruthy() + expect( + headTags.find((el) => el.attribs.src === '/hello2.js') + ).toBeTruthy() + expect( + headTags.find((el) => el.attribs.src === '/another.js') + ).toBeTruthy() + }) + + it('should pass params to head for dynamic path', async () => { + const html = await renderViaHTTP(next.url, '/blog/post-1') + const $ = cheerio.load(html) + const headTags = $('head').children().toArray() + + expect( + headTags.find( + (el) => + el.attribs.src === '/hello3.js' && + el.attribs['data-slug'] === 'post-1' + ) + ).toBeTruthy() + expect( + headTags.find((el) => el.attribs.src === '/another.js') + ).toBeTruthy() + }) + } + + runTests() +}) diff --git a/test/e2e/app-dir/head/app/blog/[slug]/page.js b/test/e2e/app-dir/head/app/blog/[slug]/page.js new file mode 100644 index 000000000000000..85783786052cc63 --- /dev/null +++ b/test/e2e/app-dir/head/app/blog/[slug]/page.js @@ -0,0 +1,18 @@ +export default function Page() { + return ( + <> +

dynamic blog page

+ + ) +} + +export async function Head({ params }) { + return ( + <> +