From 6df6f066cca159b71a6485fdce961aa3c1868961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20MANCA?= Date: Mon, 4 Nov 2024 13:23:22 +0100 Subject: [PATCH 1/3] fix(next): use the asset prefix when loading a CSS in App Router The current behavior loads the CSS with only the pathname and so, it results with a 404. The new behavior uses the full url of the asset. --- .../build/webpack/config/blocks/css/index.ts | 2 +- .../next.config.js | 3 + .../src/Component2.jsx | 27 + .../src/Component2.module.scss | 13 + .../src/Content.jsx | 29 + .../src/Content.module.css | 13 + .../src/Content4.module.css | 1067 +++++++++++++++++ .../src/app/layout.tsx | 16 + .../src/app/test-app/page.tsx | 20 + .../src/inner/k.jsx | 3 + .../src/pages/index.jsx | 24 + .../test/index.test.js | 169 +++ .../tsconfig.json | 25 + .../next-dynamic-css/src/app/layout.tsx | 16 + .../src/app/test-app/page.tsx | 20 + .../next-dynamic-css/test/index.test.js | 16 +- .../next-dynamic-css/tsconfig.json | 25 + 17 files changed, 1486 insertions(+), 2 deletions(-) create mode 100644 test/integration/next-dynamic-css-asset-prefix/next.config.js create mode 100644 test/integration/next-dynamic-css-asset-prefix/src/Component2.jsx create mode 100644 test/integration/next-dynamic-css-asset-prefix/src/Component2.module.scss create mode 100644 test/integration/next-dynamic-css-asset-prefix/src/Content.jsx create mode 100644 test/integration/next-dynamic-css-asset-prefix/src/Content.module.css create mode 100644 test/integration/next-dynamic-css-asset-prefix/src/Content4.module.css create mode 100644 test/integration/next-dynamic-css-asset-prefix/src/app/layout.tsx create mode 100644 test/integration/next-dynamic-css-asset-prefix/src/app/test-app/page.tsx create mode 100644 test/integration/next-dynamic-css-asset-prefix/src/inner/k.jsx create mode 100644 test/integration/next-dynamic-css-asset-prefix/src/pages/index.jsx create mode 100644 test/integration/next-dynamic-css-asset-prefix/test/index.test.js create mode 100644 test/integration/next-dynamic-css-asset-prefix/tsconfig.json create mode 100644 test/integration/next-dynamic-css/src/app/layout.tsx create mode 100644 test/integration/next-dynamic-css/src/app/test-app/page.tsx create mode 100644 test/integration/next-dynamic-css/tsconfig.json diff --git a/packages/next/src/build/webpack/config/blocks/css/index.ts b/packages/next/src/build/webpack/config/blocks/css/index.ts index 99d3f0117e8d2..b6f5bf6c05f4b 100644 --- a/packages/next/src/build/webpack/config/blocks/css/index.ts +++ b/packages/next/src/build/webpack/config/blocks/css/index.ts @@ -619,7 +619,7 @@ export const css = curry(async function css( insert: function (linkTag: HTMLLinkElement) { if (typeof _N_E_STYLE_LOAD === 'function') { const { href, onload, onerror } = linkTag - _N_E_STYLE_LOAD(new URL(href).pathname).then( + _N_E_STYLE_LOAD(new URL(href).toString()).then( () => onload?.call(linkTag, { type: 'load' } as Event), () => onerror?.call(linkTag, {} as Event) ) diff --git a/test/integration/next-dynamic-css-asset-prefix/next.config.js b/test/integration/next-dynamic-css-asset-prefix/next.config.js new file mode 100644 index 0000000000000..58a0de71a8500 --- /dev/null +++ b/test/integration/next-dynamic-css-asset-prefix/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + assetPrefix: 'http://localhost:__CDN_PORT__/path-prefix', +} diff --git a/test/integration/next-dynamic-css-asset-prefix/src/Component2.jsx b/test/integration/next-dynamic-css-asset-prefix/src/Component2.jsx new file mode 100644 index 0000000000000..fa1681a821668 --- /dev/null +++ b/test/integration/next-dynamic-css-asset-prefix/src/Component2.jsx @@ -0,0 +1,27 @@ +import styles from './Component2.module.scss' + +export default function Content2() { + return ( +
+

Where does it come from?

+
+ Contrary to popular belief, Lorem Ipsum is not simply random text. It + has roots in a piece of classical Latin literature from 45 BC, making it + over 2000 years old. Richard McClintock, a Latin professor at + Hampden-Sydney College in Virginia, looked up one of the more obscure + Latin words, consectetur, from a Lorem Ipsum passage, and going through + the cites of the word in classical literature, discovered the + undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 + of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by + Cicero, written in 45 BC. This book is a treatise on the theory of + ethics, very popular during the Renaissance. The first line of Lorem + Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section + 1.10.32. The standard chunk of Lorem Ipsum used since the 1500s is + reproduced below for those interested. Sections 1.10.32 and 1.10.33 from + "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their + exact original form, accompanied by English versions from the 1914 + translation by H. Rackham. +
+
+ ) +} diff --git a/test/integration/next-dynamic-css-asset-prefix/src/Component2.module.scss b/test/integration/next-dynamic-css-asset-prefix/src/Component2.module.scss new file mode 100644 index 0000000000000..c899a8eb6492e --- /dev/null +++ b/test/integration/next-dynamic-css-asset-prefix/src/Component2.module.scss @@ -0,0 +1,13 @@ +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} diff --git a/test/integration/next-dynamic-css-asset-prefix/src/Content.jsx b/test/integration/next-dynamic-css-asset-prefix/src/Content.jsx new file mode 100644 index 0000000000000..7b46fb3ede7be --- /dev/null +++ b/test/integration/next-dynamic-css-asset-prefix/src/Content.jsx @@ -0,0 +1,29 @@ +import styles from './Content.module.css' +import Content2 from './Component2' + +export default function Content() { + return ( +
+

Where does it come from?

+
+ Contrary to popular belief, Lorem Ipsum is not simply random text. It + has roots in a piece of classical Latin literature from 45 BC, making it + over 2000 years old. Richard McClintock, a Latin professor at + Hampden-Sydney College in Virginia, looked up one of the more obscure + Latin words, consectetur, from a Lorem Ipsum passage, and going through + the cites of the word in classical literature, discovered the + undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 + of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by + Cicero, written in 45 BC. This book is a treatise on the theory of + ethics, very popular during the Renaissance. The first line of Lorem + Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section + 1.10.32. The standard chunk of Lorem Ipsum used since the 1500s is + reproduced below for those interested. Sections 1.10.32 and 1.10.33 from + "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their + exact original form, accompanied by English versions from the 1914 + translation by H. Rackham. +
+ +
+ ) +} diff --git a/test/integration/next-dynamic-css-asset-prefix/src/Content.module.css b/test/integration/next-dynamic-css-asset-prefix/src/Content.module.css new file mode 100644 index 0000000000000..c899a8eb6492e --- /dev/null +++ b/test/integration/next-dynamic-css-asset-prefix/src/Content.module.css @@ -0,0 +1,13 @@ +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} diff --git a/test/integration/next-dynamic-css-asset-prefix/src/Content4.module.css b/test/integration/next-dynamic-css-asset-prefix/src/Content4.module.css new file mode 100644 index 0000000000000..c0fda1fa58a6e --- /dev/null +++ b/test/integration/next-dynamic-css-asset-prefix/src/Content4.module.css @@ -0,0 +1,1067 @@ +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} + +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} +.header { + font-style: italic; +} + +.container { + background-color: #dddddd; + padding: 1rem; +} + +.textContent { + color: #666; + letter-spacing: -1px; +} diff --git a/test/integration/next-dynamic-css-asset-prefix/src/app/layout.tsx b/test/integration/next-dynamic-css-asset-prefix/src/app/layout.tsx new file mode 100644 index 0000000000000..a14e64fcd5e33 --- /dev/null +++ b/test/integration/next-dynamic-css-asset-prefix/src/app/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/integration/next-dynamic-css-asset-prefix/src/app/test-app/page.tsx b/test/integration/next-dynamic-css-asset-prefix/src/app/test-app/page.tsx new file mode 100644 index 0000000000000..da2e183be2c37 --- /dev/null +++ b/test/integration/next-dynamic-css-asset-prefix/src/app/test-app/page.tsx @@ -0,0 +1,20 @@ +'use client' + +import React, { useState } from 'react' +import style from '../../Content4.module.css' +import { Comp } from '../../inner/k' + +export default function Index() { + const [s] = useState(true) + + if (s) { + return ( + <> +
+ + + ) + } + + return null +} diff --git a/test/integration/next-dynamic-css-asset-prefix/src/inner/k.jsx b/test/integration/next-dynamic-css-asset-prefix/src/inner/k.jsx new file mode 100644 index 0000000000000..56b2a28adc462 --- /dev/null +++ b/test/integration/next-dynamic-css-asset-prefix/src/inner/k.jsx @@ -0,0 +1,3 @@ +import dynamic from 'next/dynamic' + +export const Comp = dynamic(() => import('../Content'), { ssr: false }) diff --git a/test/integration/next-dynamic-css-asset-prefix/src/pages/index.jsx b/test/integration/next-dynamic-css-asset-prefix/src/pages/index.jsx new file mode 100644 index 0000000000000..8e13520c289c1 --- /dev/null +++ b/test/integration/next-dynamic-css-asset-prefix/src/pages/index.jsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react' +import style from '../Content4.module.css' +import { Comp } from '../inner/k' + +export default function Index() { + const [s] = useState(true) + + if (s) { + return ( + <> +
+ + + ) + } + + return null +} + +export const getServerSideProps = () => { + return { + props: {}, + } +} diff --git a/test/integration/next-dynamic-css-asset-prefix/test/index.test.js b/test/integration/next-dynamic-css-asset-prefix/test/index.test.js new file mode 100644 index 0000000000000..c707a4471d65a --- /dev/null +++ b/test/integration/next-dynamic-css-asset-prefix/test/index.test.js @@ -0,0 +1,169 @@ +/* eslint-env jest */ + +import webdriver from 'next-webdriver' +import { createServer, request } from 'http' +import { join, resolve } from 'path' +import { + findPort, + launchApp, + killApp, + nextBuild, + nextStart, + File, +} from 'next-test-utils' + +const appDir = join(__dirname, '..') +const nextConfig = new File(resolve(__dirname, '../next.config.js')) + +let appPort +let cdnPort +let app +let cdn + +function runTests() { + it('should load a Pages Router page correctly', async () => { + const browser = await webdriver(appPort, '/') + + expect( + await browser + .elementByCss('#__next div:nth-child(2)') + .getComputedCss('background-color') + ).toContain('221, 221, 221') + + expect(await browser.eval('document.documentElement.innerHTML')).toContain( + 'Where does it come from?' + ) + }) + + it('should load a App Router page correctly', async () => { + const browser = await webdriver(appPort, '/test-app') + + expect( + await browser + .elementByCss('body div:nth-child(3)') + .getComputedCss('background-color') + ).toContain('221, 221, 221') + + expect(await browser.eval('document.documentElement.innerHTML')).toContain( + 'Where does it come from?' + ) + }) +} + +describe('next/dynamic with assetPrefix', () => { + ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( + 'development mode', + () => { + beforeAll(async () => { + cdnPort = await findPort() + // lightweight http proxy + cdn = createServer((clientReq, clientRes) => { + const proxyPath = clientReq.url.slice('/path-prefix'.length) + const proxyReq = request( + { + hostname: 'localhost', + port: appPort, + path: proxyPath, + method: clientReq.method, + headers: clientReq.headers, + }, + (proxyRes) => { + // cdn must be configured to allow requests from this origin + proxyRes.headers['Access-Control-Allow-Origin'] = + `http://localhost:${appPort}` + clientRes.writeHead(proxyRes.statusCode, proxyRes.headers) + // [NOTE] if socket doesn't have a handler to error event and if error + // event leaks, node.js ends its process with errored exit code. + // However, there can be failing socket event while running test + // as long as assertion is correct, do not care indiviual socket errors. + proxyRes.on('error', (e) => { + require('console').error(e) + }) + clientRes.on('error', (e) => { + require('console').error(e) + }) + + proxyRes.pipe(clientRes, { end: true }) + } + ) + + proxyReq.on('error', (e) => { + require('console').error(e) + }) + clientReq.on('error', (e) => { + require('console').error(e) + }) + clientReq.pipe(proxyReq, { end: true }) + }) + await new Promise((resolve) => cdn.listen(cdnPort, resolve)) + nextConfig.replace('__CDN_PORT__', cdnPort) + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + + afterAll(() => killApp(app)) + afterAll(() => cdn.close()) + afterAll(() => nextConfig.restore()) + + runTests(true) + } + ) + ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( + 'production mode', + () => { + beforeAll(async () => { + cdnPort = await findPort() + // lightweight http proxy + cdn = createServer((clientReq, clientRes) => { + const proxyPath = clientReq.url.slice('/path-prefix'.length) + const proxyReq = request( + { + hostname: 'localhost', + port: appPort, + path: proxyPath, + method: clientReq.method, + headers: clientReq.headers, + }, + (proxyRes) => { + // cdn must be configured to allow requests from this origin + proxyRes.headers['Access-Control-Allow-Origin'] = + `http://localhost:${appPort}` + clientRes.writeHead(proxyRes.statusCode, proxyRes.headers) + // [NOTE] if socket doesn't have a handler to error event and if error + // event leaks, node.js ends its process with errored exit code. + // However, there can be failing socket event while running test + // as long as assertion is correct, do not care indiviual socket errors. + proxyRes.on('error', (e) => { + require('console').error(e) + }) + clientRes.on('error', (e) => { + require('console').error(e) + }) + + proxyRes.pipe(clientRes, { end: true }) + } + ) + + proxyReq.on('error', (e) => { + require('console').error(e) + }) + clientReq.on('error', (e) => { + require('console').error(e) + }) + clientReq.pipe(proxyReq, { end: true }) + }) + await new Promise((resolve) => cdn.listen(cdnPort, resolve)) + nextConfig.replace('__CDN_PORT__', cdnPort) + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + + afterAll(() => killApp(app)) + afterAll(() => cdn.close()) + afterAll(() => nextConfig.restore()) + + runTests() + } + ) +}) diff --git a/test/integration/next-dynamic-css-asset-prefix/tsconfig.json b/test/integration/next-dynamic-css-asset-prefix/tsconfig.json new file mode 100644 index 0000000000000..ed02d0b66740b --- /dev/null +++ b/test/integration/next-dynamic-css-asset-prefix/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ], + "strictNullChecks": true + }, + "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/test/integration/next-dynamic-css/src/app/layout.tsx b/test/integration/next-dynamic-css/src/app/layout.tsx new file mode 100644 index 0000000000000..a14e64fcd5e33 --- /dev/null +++ b/test/integration/next-dynamic-css/src/app/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/integration/next-dynamic-css/src/app/test-app/page.tsx b/test/integration/next-dynamic-css/src/app/test-app/page.tsx new file mode 100644 index 0000000000000..da2e183be2c37 --- /dev/null +++ b/test/integration/next-dynamic-css/src/app/test-app/page.tsx @@ -0,0 +1,20 @@ +'use client' + +import React, { useState } from 'react' +import style from '../../Content4.module.css' +import { Comp } from '../../inner/k' + +export default function Index() { + const [s] = useState(true) + + if (s) { + return ( + <> +
+ + + ) + } + + return null +} diff --git a/test/integration/next-dynamic-css/test/index.test.js b/test/integration/next-dynamic-css/test/index.test.js index 77a4ed94019cd..c3f1603a65551 100644 --- a/test/integration/next-dynamic-css/test/index.test.js +++ b/test/integration/next-dynamic-css/test/index.test.js @@ -15,7 +15,7 @@ let appPort const appDir = join(__dirname, '../') function runTests() { - it('should load page correctly', async () => { + it('should load a Pages Router page correctly', async () => { const browser = await webdriver(appPort, '/') expect( @@ -28,6 +28,20 @@ function runTests() { 'Where does it come from?' ) }) + + it('should load a App Router page correctly', async () => { + const browser = await webdriver(appPort, '/test-app') + + expect( + await browser + .elementByCss('body div:nth-child(3)') + .getComputedCss('background-color') + ).toContain('221, 221, 221') + + expect(await browser.eval('document.documentElement.innerHTML')).toContain( + 'Where does it come from?' + ) + }) } describe('next/dynamic', () => { diff --git a/test/integration/next-dynamic-css/tsconfig.json b/test/integration/next-dynamic-css/tsconfig.json new file mode 100644 index 0000000000000..ed02d0b66740b --- /dev/null +++ b/test/integration/next-dynamic-css/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ], + "strictNullChecks": true + }, + "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} From 619a7e671770ef28fca56052c89e6f572741c379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20MANCA?= Date: Mon, 9 Dec 2024 10:26:35 +0100 Subject: [PATCH 2/3] fix: use href with no URL transformation Co-authored-by: Tobias Koppers --- packages/next/src/build/webpack/config/blocks/css/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/src/build/webpack/config/blocks/css/index.ts b/packages/next/src/build/webpack/config/blocks/css/index.ts index b6f5bf6c05f4b..e361e770218c2 100644 --- a/packages/next/src/build/webpack/config/blocks/css/index.ts +++ b/packages/next/src/build/webpack/config/blocks/css/index.ts @@ -619,7 +619,7 @@ export const css = curry(async function css( insert: function (linkTag: HTMLLinkElement) { if (typeof _N_E_STYLE_LOAD === 'function') { const { href, onload, onerror } = linkTag - _N_E_STYLE_LOAD(new URL(href).toString()).then( + _N_E_STYLE_LOAD(href).then( () => onload?.call(linkTag, { type: 'load' } as Event), () => onerror?.call(linkTag, {} as Event) ) From 761a02c15dfc4af2f0374723143e04796a5fd8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20MANCA?= Date: Thu, 12 Dec 2024 22:47:27 +0100 Subject: [PATCH 3/3] fix: only use the full URL when the asset origin is not the same than the client one --- packages/next/src/build/webpack/config/blocks/css/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/next/src/build/webpack/config/blocks/css/index.ts b/packages/next/src/build/webpack/config/blocks/css/index.ts index e361e770218c2..c62e1e5a27548 100644 --- a/packages/next/src/build/webpack/config/blocks/css/index.ts +++ b/packages/next/src/build/webpack/config/blocks/css/index.ts @@ -619,7 +619,11 @@ export const css = curry(async function css( insert: function (linkTag: HTMLLinkElement) { if (typeof _N_E_STYLE_LOAD === 'function') { const { href, onload, onerror } = linkTag - _N_E_STYLE_LOAD(href).then( + _N_E_STYLE_LOAD( + href.indexOf(window.location.origin) === 0 + ? new URL(href).pathname + : href + ).then( () => onload?.call(linkTag, { type: 'load' } as Event), () => onerror?.call(linkTag, {} as Event) )