diff --git a/packages/next/client/head-manager.ts b/packages/next/client/head-manager.ts index 31fa6a0c5f62e24b6389252f0a1fbec1eecce7d1..4ea0ef9597149a4e35a14d6be04682da71ab355d 100644 --- a/packages/next/client/head-manager.ts +++ b/packages/next/client/head-manager.ts @@ -3,10 +3,11 @@ export const DOMAttributeNames: Record = { className: 'class', htmlFor: 'for', httpEquiv: 'http-equiv', + noModule: 'noModule', } function reactElementToDOM({ type, props }: JSX.Element): HTMLElement { - const el = document.createElement(type) + const el: HTMLElement = document.createElement(type) for (const p in props) { if (!props.hasOwnProperty(p)) continue if (p === 'children' || p === 'dangerouslySetInnerHTML') continue @@ -15,7 +16,14 @@ function reactElementToDOM({ type, props }: JSX.Element): HTMLElement { if (props[p] === undefined) continue const attr = DOMAttributeNames[p] || p.toLowerCase() - el.setAttribute(attr, props[p]) + if ( + type === 'script' && + (attr === 'async' || attr === 'defer' || attr === 'noModule') + ) { + ;(el as HTMLScriptElement)[attr] = !!props[p] + } else { + el.setAttribute(attr, props[p]) + } } const { children, dangerouslySetInnerHTML } = props diff --git a/test/integration/client-navigation/pages/head.js b/test/integration/client-navigation/pages/head.js index 979397114f42a01e4b95fcd15e01e286b8107c1c..bd50dddcc54d46ec529b8310c5de41702c33bb29 100644 --- a/test/integration/client-navigation/pages/head.js +++ b/test/integration/client-navigation/pages/head.js @@ -115,6 +115,11 @@ export default () => ( + {/* this should not execute twice on the client */} + + {/* this should not execute twice on the client (intentionally sets defer to `yas` to test boolean coercion) */} + + {/* such style can be used for alternate links on _app vs individual pages */} {['pl', 'en'].map((language) => ( { }) describe('updating head while client routing', () => { + it('should only execute async and defer scripts once', async () => { + let browser + try { + browser = await webdriver(context.appPort, '/head') + + await browser.waitForElementByCss('h1') + await waitFor(2000) + expect( + Number(await browser.eval('window.__test_async_executions')) + ).toBe(1) + expect( + Number(await browser.eval('window.__test_defer_executions')) + ).toBe(1) + } finally { + if (browser) { + await browser.close() + } + } + }) + it('should update head during client routing', async () => { let browser try { diff --git a/test/integration/client-navigation/test/rendering.js b/test/integration/client-navigation/test/rendering.js index 7b60e912286676e9828595dd2256c6da7101ff31..817e23dc61eb7edc1cbee9c840b4f4379f82a737 100644 --- a/test/integration/client-navigation/test/rendering.js +++ b/test/integration/client-navigation/test/rendering.js @@ -186,6 +186,12 @@ export default function (render, fetch, ctx) { expect(html).toContain('') }) + test('header helper renders boolean attributes correctly children', async () => { + const html = await render('/head') + expect(html).toContain('