From f96c362eaffa9b15eb401b20b67c27eb12fc2ceb Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:11:25 +0200 Subject: [PATCH 01/10] Extend tests --- test/e2e/url/app/api/route.js | 6 + test/e2e/url/app/client/page.js | 11 ++ test/e2e/url/app/layout.js | 7 + test/e2e/url/app/manifest.js | 15 +++ test/e2e/url/app/rsc/page.js | 9 ++ test/e2e/url/middleware.ts | 13 ++ .../e2e/url/pages/api/{ => pages}/basename.js | 2 +- test/e2e/url/pages/api/{ => pages}/size.js | 2 +- test/e2e/url/pages/pages/ssg.js | 15 +++ test/e2e/url/pages/pages/ssr.js | 15 +++ test/e2e/url/pages/pages/static.js | 9 ++ test/e2e/url/pages/ssg.js | 15 --- test/e2e/url/pages/ssr.js | 15 --- test/e2e/url/pages/static.js | 9 -- test/e2e/url/url.test.ts | 123 ++++++++++++++---- 15 files changed, 198 insertions(+), 68 deletions(-) create mode 100644 test/e2e/url/app/api/route.js create mode 100644 test/e2e/url/app/client/page.js create mode 100644 test/e2e/url/app/layout.js create mode 100644 test/e2e/url/app/manifest.js create mode 100644 test/e2e/url/app/rsc/page.js create mode 100644 test/e2e/url/middleware.ts rename test/e2e/url/pages/api/{ => pages}/basename.js (63%) rename test/e2e/url/pages/api/{ => pages}/size.js (61%) create mode 100644 test/e2e/url/pages/pages/ssg.js create mode 100644 test/e2e/url/pages/pages/ssr.js create mode 100644 test/e2e/url/pages/pages/static.js delete mode 100644 test/e2e/url/pages/ssg.js delete mode 100644 test/e2e/url/pages/ssr.js delete mode 100644 test/e2e/url/pages/static.js diff --git a/test/e2e/url/app/api/route.js b/test/e2e/url/app/api/route.js new file mode 100644 index 00000000000000..8e7a6b6dfd49e9 --- /dev/null +++ b/test/e2e/url/app/api/route.js @@ -0,0 +1,6 @@ +import imported from '../../public/vercel.png' +const url = new URL('../../public/vercel.png', import.meta.url).toString() + +export function GET(req, res) { + return Response.json({ imported, url }) +} diff --git a/test/e2e/url/app/client/page.js b/test/e2e/url/app/client/page.js new file mode 100644 index 00000000000000..e1fddf79c865bc --- /dev/null +++ b/test/e2e/url/app/client/page.js @@ -0,0 +1,11 @@ +'use client' + +const url = new URL('../../public/vercel.png', import.meta.url).pathname + +export default function Index(props) { + return ( +
+ Hello {new URL('../../public/vercel.png', import.meta.url).pathname}+{url} +
+ ) +} diff --git a/test/e2e/url/app/layout.js b/test/e2e/url/app/layout.js new file mode 100644 index 00000000000000..4ee00a218505ac --- /dev/null +++ b/test/e2e/url/app/layout.js @@ -0,0 +1,7 @@ +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/url/app/manifest.js b/test/e2e/url/app/manifest.js new file mode 100644 index 00000000000000..544ecf4c6fbc63 --- /dev/null +++ b/test/e2e/url/app/manifest.js @@ -0,0 +1,15 @@ +import icon from '../public/vercel.png' + +export default function manifest() { + return { + short_name: 'Next.js', + name: 'Next.js', + icons: [ + { + src: icon.src, + type: 'image/png', + sizes: '512x512', + }, + ], + } +} diff --git a/test/e2e/url/app/rsc/page.js b/test/e2e/url/app/rsc/page.js new file mode 100644 index 00000000000000..3db56282512a5b --- /dev/null +++ b/test/e2e/url/app/rsc/page.js @@ -0,0 +1,9 @@ +const url = new URL('../../public/vercel.png', import.meta.url).pathname + +export default function Index(props) { + return ( +
+ Hello {new URL('../../public/vercel.png', import.meta.url).pathname}+{url} +
+ ) +} diff --git a/test/e2e/url/middleware.ts b/test/e2e/url/middleware.ts new file mode 100644 index 00000000000000..bf4ab3bcaa383e --- /dev/null +++ b/test/e2e/url/middleware.ts @@ -0,0 +1,13 @@ +import { NextRequest, NextResponse } from 'next/server' + +// @ts-ignore +import imported from './public/vercel.png' +const url = new URL('./public/vercel.png', import.meta.url) + +export async function middleware(req: NextRequest) { + if (req.nextUrl.pathname === '/middleware') { + return Response.json({ imported, url }) + } + + return NextResponse.next() +} diff --git a/test/e2e/url/pages/api/basename.js b/test/e2e/url/pages/api/pages/basename.js similarity index 63% rename from test/e2e/url/pages/api/basename.js rename to test/e2e/url/pages/api/pages/basename.js index 0109c99b7b8819..22104403c5bcaf 100644 --- a/test/e2e/url/pages/api/basename.js +++ b/test/e2e/url/pages/api/pages/basename.js @@ -1,6 +1,6 @@ import path from 'path' -const img = new URL('../../public/vercel.png', import.meta.url) +const img = new URL('../../../public/vercel.png', import.meta.url) export default (req, res) => { res.json({ basename: path.posix.basename(img.pathname) }) diff --git a/test/e2e/url/pages/api/size.js b/test/e2e/url/pages/api/pages/size.js similarity index 61% rename from test/e2e/url/pages/api/size.js rename to test/e2e/url/pages/api/pages/size.js index 7649a0bcb1307b..2e961a4dcd5a43 100644 --- a/test/e2e/url/pages/api/size.js +++ b/test/e2e/url/pages/api/pages/size.js @@ -1,6 +1,6 @@ import fs from 'fs' -const img = new URL('../../public/vercel.png', import.meta.url) +const img = new URL('../../../public/vercel.png', import.meta.url) export default (req, res) => { res.json({ size: fs.readFileSync(img).length }) diff --git a/test/e2e/url/pages/pages/ssg.js b/test/e2e/url/pages/pages/ssg.js new file mode 100644 index 00000000000000..d6a4221eb23abf --- /dev/null +++ b/test/e2e/url/pages/pages/ssg.js @@ -0,0 +1,15 @@ +export async function getStaticProps() { + return { + props: { + url: new URL('../../public/vercel.png', import.meta.url).pathname, + }, + } +} + +export default function Index({ url }) { + return ( +
+ Hello {new URL('../../public/vercel.png', import.meta.url).pathname}+{url} +
+ ) +} diff --git a/test/e2e/url/pages/pages/ssr.js b/test/e2e/url/pages/pages/ssr.js new file mode 100644 index 00000000000000..d2ce01363cec01 --- /dev/null +++ b/test/e2e/url/pages/pages/ssr.js @@ -0,0 +1,15 @@ +export function getServerSideProps() { + return { + props: { + url: new URL('../../public/vercel.png', import.meta.url).pathname, + }, + } +} + +export default function Index({ url }) { + return ( +
+ Hello {new URL('../../public/vercel.png', import.meta.url).pathname}+{url} +
+ ) +} diff --git a/test/e2e/url/pages/pages/static.js b/test/e2e/url/pages/pages/static.js new file mode 100644 index 00000000000000..3db56282512a5b --- /dev/null +++ b/test/e2e/url/pages/pages/static.js @@ -0,0 +1,9 @@ +const url = new URL('../../public/vercel.png', import.meta.url).pathname + +export default function Index(props) { + return ( +
+ Hello {new URL('../../public/vercel.png', import.meta.url).pathname}+{url} +
+ ) +} diff --git a/test/e2e/url/pages/ssg.js b/test/e2e/url/pages/ssg.js deleted file mode 100644 index 113f47c56552f6..00000000000000 --- a/test/e2e/url/pages/ssg.js +++ /dev/null @@ -1,15 +0,0 @@ -export async function getStaticProps() { - return { - props: { - url: new URL('../public/vercel.png', import.meta.url).pathname, - }, - } -} - -export default function Index({ url }) { - return ( -
- Hello {new URL('../public/vercel.png', import.meta.url).pathname}+{url} -
- ) -} diff --git a/test/e2e/url/pages/ssr.js b/test/e2e/url/pages/ssr.js deleted file mode 100644 index 6aec1e94a0f725..00000000000000 --- a/test/e2e/url/pages/ssr.js +++ /dev/null @@ -1,15 +0,0 @@ -export function getServerSideProps() { - return { - props: { - url: new URL('../public/vercel.png', import.meta.url).pathname, - }, - } -} - -export default function Index({ url }) { - return ( -
- Hello {new URL('../public/vercel.png', import.meta.url).pathname}+{url} -
- ) -} diff --git a/test/e2e/url/pages/static.js b/test/e2e/url/pages/static.js deleted file mode 100644 index 03bc185ce9f08b..00000000000000 --- a/test/e2e/url/pages/static.js +++ /dev/null @@ -1,9 +0,0 @@ -const url = new URL('../public/vercel.png', import.meta.url).pathname - -export default function Index(props) { - return ( -
- Hello {new URL('../public/vercel.png', import.meta.url).pathname}+{url} -
- ) -} diff --git a/test/e2e/url/url.test.ts b/test/e2e/url/url.test.ts index 7a11eff4aaa658..cfa397bed816db 100644 --- a/test/e2e/url/url.test.ts +++ b/test/e2e/url/url.test.ts @@ -1,4 +1,4 @@ -import { getBrowserBodyText, retry } from 'next-test-utils' +import { retry } from 'next-test-utils' import { nextTestSetup } from 'e2e-utils' describe(`Handle new URL asset references`, () => { @@ -6,41 +6,110 @@ describe(`Handle new URL asset references`, () => { files: __dirname, }) - const expectedServer = - /Hello \/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png\+\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png/ - const expectedClient = new RegExp( - expectedServer.source.replace(//g, '') - ) - - for (const page of ['/static', '/ssr', '/ssg']) { - it(`should render the ${page} page`, async () => { - const html = await next.render(page) - expect(html).toMatch(expectedServer) - }) - - it(`should client-render the ${page} page`, async () => { - const browser = await next.browser(page) - await retry(async () => - expect(await getBrowserBodyText(browser)).toMatch(expectedClient) - ) - }) - } - - it('should respond on size api', async () => { + it('should respond on middleware api', async () => { const data = await next - .fetch('/api/size') + .fetch('/middleware') .then((res) => res.ok && res.json()) - expect(data).toEqual({ size: 30079 }) + expect(data).toEqual({ + imported: expect.objectContaining({ + src: expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ), + }), + url: expect.stringMatching(/blob:vercel\.[0-9a-f]{8,}\.png$/), + }) }) - it('should respond on basename api', async () => { + it('should respond on webmanifest', async () => { const data = await next - .fetch('/api/basename') + .fetch('/manifest.webmanifest') .then((res) => res.ok && res.json()) expect(data).toEqual({ - basename: expect.stringMatching(/^vercel\.[0-9a-f]{8}\.png$/), + short_name: 'Next.js', + name: 'Next.js', + icons: [ + { + src: expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ), + type: 'image/png', + sizes: '512x512', + }, + ], + }) + }) + + const expectedPage = + /Hello \/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png\+\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png/ + + describe('app router', () => { + for (const page of ['/rsc', '/client']) { + it(`should render the ${page} page`, async () => { + const $ = await next.render$(page) + expect($('main').text()).toMatch(expectedPage) + }) + + it(`should client-render the ${page} page`, async () => { + const browser = await next.browser(page) + await retry(async () => + expect(await browser.elementByCss('main').text()).toMatch( + expectedPage + ) + ) + }) + } + + it('should respond on API', async () => { + const data = await next.fetch('/api').then((res) => res.ok && res.json()) + + expect(data).toEqual({ + imported: expect.objectContaining({ + src: expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ), + }), + url: expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ), + }) + }) + }) + + describe('pages router', () => { + for (const page of ['/pages/static', '/pages/ssr', '/pages/ssg']) { + it(`should render the ${page} page`, async () => { + const $ = await next.render$(page) + expect($('main').text()).toMatch(expectedPage) + }) + + it(`should client-render the ${page} page`, async () => { + const browser = await next.browser(page) + await retry(async () => + expect(await browser.elementByCss('main').text()).toMatch( + expectedPage + ) + ) + }) + } + + it('should respond on size api', async () => { + const data = await next + .fetch('/api/pages/size') + .then((res) => res.ok && res.json()) + + expect(data).toEqual({ size: 30079 }) + }) + + it('should respond on basename api', async () => { + const data = await next + .fetch('/api/pages/basename') + .then((res) => res.ok && res.json()) + + expect(data).toEqual({ + basename: expect.stringMatching(/^vercel\.[0-9a-f]{8}\.png$/), + }) }) }) }) From a7a09b617cddef1dc18fe270b319e7c0909a3248 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:26:25 +0200 Subject: [PATCH 02/10] f test --- test/e2e/url/app/client/page.js | 5 +- test/e2e/url/app/manifest.js | 2 + test/e2e/url/app/rsc/page.js | 5 +- test/e2e/url/middleware.ts | 4 +- test/e2e/url/pages/api/pages/basename.js | 7 -- test/e2e/url/pages/api/pages/index.js | 12 ++++ test/e2e/url/pages/api/pages/size.js | 7 -- test/e2e/url/pages/pages/ssg.js | 7 +- test/e2e/url/pages/pages/ssr.js | 7 +- test/e2e/url/pages/pages/static.js | 5 +- test/e2e/url/url.test.ts | 85 ++++++++++++++---------- 11 files changed, 84 insertions(+), 62 deletions(-) delete mode 100644 test/e2e/url/pages/api/pages/basename.js create mode 100644 test/e2e/url/pages/api/pages/index.js delete mode 100644 test/e2e/url/pages/api/pages/size.js diff --git a/test/e2e/url/app/client/page.js b/test/e2e/url/app/client/page.js index e1fddf79c865bc..810acb958590dd 100644 --- a/test/e2e/url/app/client/page.js +++ b/test/e2e/url/app/client/page.js @@ -1,11 +1,12 @@ 'use client' -const url = new URL('../../public/vercel.png', import.meta.url).pathname +import imported from '../../public/vercel.png' +const url = new URL('../../public/vercel.png', import.meta.url).toString() export default function Index(props) { return (
- Hello {new URL('../../public/vercel.png', import.meta.url).pathname}+{url} + Hello {imported.src}+{url}
) } diff --git a/test/e2e/url/app/manifest.js b/test/e2e/url/app/manifest.js index 544ecf4c6fbc63..b19203108ff7d9 100644 --- a/test/e2e/url/app/manifest.js +++ b/test/e2e/url/app/manifest.js @@ -1,4 +1,5 @@ import icon from '../public/vercel.png' +const url = new URL('../public/vercel.png', import.meta.url).toString() export default function manifest() { return { @@ -11,5 +12,6 @@ export default function manifest() { sizes: '512x512', }, ], + description: url, } } diff --git a/test/e2e/url/app/rsc/page.js b/test/e2e/url/app/rsc/page.js index 3db56282512a5b..ce35c94a1ec36d 100644 --- a/test/e2e/url/app/rsc/page.js +++ b/test/e2e/url/app/rsc/page.js @@ -1,9 +1,10 @@ -const url = new URL('../../public/vercel.png', import.meta.url).pathname +import imported from '../../public/vercel.png' +const url = new URL('../../public/vercel.png', import.meta.url).toString() export default function Index(props) { return (
- Hello {new URL('../../public/vercel.png', import.meta.url).pathname}+{url} + Hello {imported.src}+{url}
) } diff --git a/test/e2e/url/middleware.ts b/test/e2e/url/middleware.ts index bf4ab3bcaa383e..1d87d3d5bb57c1 100644 --- a/test/e2e/url/middleware.ts +++ b/test/e2e/url/middleware.ts @@ -2,10 +2,10 @@ import { NextRequest, NextResponse } from 'next/server' // @ts-ignore import imported from './public/vercel.png' -const url = new URL('./public/vercel.png', import.meta.url) +const url = new URL('./public/vercel.png', import.meta.url).toString() export async function middleware(req: NextRequest) { - if (req.nextUrl.pathname === '/middleware') { + if (req.nextUrl.toString().endsWith('/middleware')) { return Response.json({ imported, url }) } diff --git a/test/e2e/url/pages/api/pages/basename.js b/test/e2e/url/pages/api/pages/basename.js deleted file mode 100644 index 22104403c5bcaf..00000000000000 --- a/test/e2e/url/pages/api/pages/basename.js +++ /dev/null @@ -1,7 +0,0 @@ -import path from 'path' - -const img = new URL('../../../public/vercel.png', import.meta.url) - -export default (req, res) => { - res.json({ basename: path.posix.basename(img.pathname) }) -} diff --git a/test/e2e/url/pages/api/pages/index.js b/test/e2e/url/pages/api/pages/index.js new file mode 100644 index 00000000000000..17347a6eaa50d1 --- /dev/null +++ b/test/e2e/url/pages/api/pages/index.js @@ -0,0 +1,12 @@ +import fs from 'fs' + +import imported from '../../../public/vercel.png' +const url = new URL('../../../public/vercel.png', import.meta.url) + +export default (req, res) => { + res.json({ + imported, + url: url.toString(), + size: fs.readFileSync(url).length, + }) +} diff --git a/test/e2e/url/pages/api/pages/size.js b/test/e2e/url/pages/api/pages/size.js deleted file mode 100644 index 2e961a4dcd5a43..00000000000000 --- a/test/e2e/url/pages/api/pages/size.js +++ /dev/null @@ -1,7 +0,0 @@ -import fs from 'fs' - -const img = new URL('../../../public/vercel.png', import.meta.url) - -export default (req, res) => { - res.json({ size: fs.readFileSync(img).length }) -} diff --git a/test/e2e/url/pages/pages/ssg.js b/test/e2e/url/pages/pages/ssg.js index d6a4221eb23abf..fe8d852cca225a 100644 --- a/test/e2e/url/pages/pages/ssg.js +++ b/test/e2e/url/pages/pages/ssg.js @@ -1,7 +1,9 @@ +import imported from '../../public/vercel.png' + export async function getStaticProps() { return { props: { - url: new URL('../../public/vercel.png', import.meta.url).pathname, + url: new URL('../../public/vercel.png', import.meta.url).toString(), }, } } @@ -9,7 +11,8 @@ export async function getStaticProps() { export default function Index({ url }) { return (
- Hello {new URL('../../public/vercel.png', import.meta.url).pathname}+{url} + Hello {imported.src}+ + {new URL('../../public/vercel.png', import.meta.url).toString()}+{url}
) } diff --git a/test/e2e/url/pages/pages/ssr.js b/test/e2e/url/pages/pages/ssr.js index d2ce01363cec01..ca2572a4b85157 100644 --- a/test/e2e/url/pages/pages/ssr.js +++ b/test/e2e/url/pages/pages/ssr.js @@ -1,7 +1,9 @@ +import imported from '../../public/vercel.png' + export function getServerSideProps() { return { props: { - url: new URL('../../public/vercel.png', import.meta.url).pathname, + url: new URL('../../public/vercel.png', import.meta.url).toString(), }, } } @@ -9,7 +11,8 @@ export function getServerSideProps() { export default function Index({ url }) { return (
- Hello {new URL('../../public/vercel.png', import.meta.url).pathname}+{url} + Hello {imported.src}+ + {new URL('../../public/vercel.png', import.meta.url).toString()}+{url}
) } diff --git a/test/e2e/url/pages/pages/static.js b/test/e2e/url/pages/pages/static.js index 3db56282512a5b..ce35c94a1ec36d 100644 --- a/test/e2e/url/pages/pages/static.js +++ b/test/e2e/url/pages/pages/static.js @@ -1,9 +1,10 @@ -const url = new URL('../../public/vercel.png', import.meta.url).pathname +import imported from '../../public/vercel.png' +const url = new URL('../../public/vercel.png', import.meta.url).toString() export default function Index(props) { return (
- Hello {new URL('../../public/vercel.png', import.meta.url).pathname}+{url} + Hello {imported.src}+{url}
) } diff --git a/test/e2e/url/url.test.ts b/test/e2e/url/url.test.ts index cfa397bed816db..eb5cd5726bfcff 100644 --- a/test/e2e/url/url.test.ts +++ b/test/e2e/url/url.test.ts @@ -1,6 +1,11 @@ import { retry } from 'next-test-utils' import { nextTestSetup } from 'e2e-utils' +// | | Pages Client | Pages Server (SSR,RSC) | API Routes/Middleware | Metadata Routes | +// |---------|-------------------------|-------------------------|-------------------------|-------------------------| +// | new URL | /_next/static/media/... | /_next/static/media/... | /server/assets/... | /_next/static/media/... | +// | import | /_next/static/media/... | /_next/static/media/... | /_next/static/media/... | /_next/static/media/... | +// |---------|-------------------------|-------------------------|-------------------------|-------------------------| describe(`Handle new URL asset references`, () => { const { next } = nextTestSetup({ files: __dirname, @@ -17,34 +22,37 @@ describe(`Handle new URL asset references`, () => { /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ ), }), - url: expect.stringMatching(/blob:vercel\.[0-9a-f]{8,}\.png$/), - }) - }) - - it('should respond on webmanifest', async () => { - const data = await next - .fetch('/manifest.webmanifest') - .then((res) => res.ok && res.json()) - - expect(data).toEqual({ - short_name: 'Next.js', - name: 'Next.js', - icons: [ - { - src: expect.stringMatching( - /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ - ), - type: 'image/png', - sizes: '512x512', - }, - ], + url: expect.stringMatching(/^blob:.*vercel\.[0-9a-f]{8,}\.png$/), }) }) const expectedPage = - /Hello \/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png\+\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png/ + /^Hello \/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png(\+\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png(\+\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png)?)?$/ describe('app router', () => { + it('should respond on webmanifest', async () => { + const data = await next + .fetch('/manifest.webmanifest') + .then((res) => res.ok && res.json()) + + expect(data).toEqual({ + short_name: 'Next.js', + name: 'Next.js', + icons: [ + { + src: expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ), + type: 'image/png', + sizes: '512x512', + }, + ], + description: expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ), + }) + }) + for (const page of ['/rsc', '/client']) { it(`should render the ${page} page`, async () => { const $ = await next.render$(page) @@ -70,9 +78,14 @@ describe(`Handle new URL asset references`, () => { /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ ), }), - url: expect.stringMatching( - /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ - ), + // TODO Webpack bug? + url: process.env.IS_TURBOPACK_TEST + ? expect.stringMatching( + /file:.*\/.next\/server\/.*\/vercel\.[0-9a-f]{8}\.png$/ + ) + : expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ), }) }) }) @@ -94,21 +107,21 @@ describe(`Handle new URL asset references`, () => { }) } - it('should respond on size api', async () => { - const data = await next - .fetch('/api/pages/size') - .then((res) => res.ok && res.json()) - - expect(data).toEqual({ size: 30079 }) - }) - - it('should respond on basename api', async () => { + it('should respond on API', async () => { const data = await next - .fetch('/api/pages/basename') + .fetch('/api/pages/') .then((res) => res.ok && res.json()) expect(data).toEqual({ - basename: expect.stringMatching(/^vercel\.[0-9a-f]{8}\.png$/), + imported: expect.objectContaining({ + src: expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ), + }), + url: expect.stringMatching( + /file:.*\/.next\/server\/.*\/vercel\.[0-9a-f]{8}\.png$/ + ), + size: 30079, }) }) }) From 8bd09d3da2a4acdafed1a1ea49c15169243b7202 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:13:48 +0200 Subject: [PATCH 03/10] wip --- crates/next-api/src/project.rs | 14 ++++- crates/next-core/src/next_edge/context.rs | 9 ++- crates/next-core/src/next_image/module.rs | 4 +- crates/next-core/src/next_server/context.rs | 5 ++ .../turbopack-browser/src/chunking_context.rs | 63 ++++++++++++++++--- .../src/chunk/chunking_context.rs | 3 +- .../turbopack-nodejs/src/chunking_context.rs | 59 +++++++++++++++-- turbopack/crates/turbopack-static/src/css.rs | 9 +-- turbopack/crates/turbopack-static/src/ecma.rs | 22 ++++--- .../turbopack-static/src/output_asset.rs | 12 +++- turbopack/crates/turbopack/src/lib.rs | 11 ++-- .../turbopack/src/module_options/mod.rs | 16 ++++- .../src/module_options/module_rule.rs | 5 +- 13 files changed, 189 insertions(+), 43 deletions(-) diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index 1f142320e8e591..d2571c9a94af99 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -1089,7 +1089,11 @@ impl Project { self.next_config().computed_asset_prefix().owned().await?, ) } else { - get_server_chunking_context(options) + get_server_chunking_context( + options, + self.client_relative_path().owned().await?, + self.next_config().computed_asset_prefix().owned().await?, + ) }) } @@ -1115,10 +1119,14 @@ impl Project { get_edge_chunking_context_with_client_assets( options, self.client_relative_path().owned().await?, - self.next_config().computed_asset_prefix(), + self.next_config().computed_asset_prefix().owned().await?, ) } else { - get_edge_chunking_context(options) + get_edge_chunking_context( + options, + self.client_relative_path().owned().await?, + self.next_config().computed_asset_prefix().owned().await?, + ) }) } diff --git a/crates/next-core/src/next_edge/context.rs b/crates/next-core/src/next_edge/context.rs index cd780efb06015e..091d734f40ccac 100644 --- a/crates/next-core/src/next_edge/context.rs +++ b/crates/next-core/src/next_edge/context.rs @@ -210,7 +210,7 @@ pub struct EdgeChunkingContextOptions { pub async fn get_edge_chunking_context_with_client_assets( options: EdgeChunkingContextOptions, client_root: FileSystemPath, - asset_prefix: ResolvedVc>, + asset_prefix: Option, ) -> Result>> { let EdgeChunkingContextOptions { mode, @@ -237,7 +237,7 @@ pub async fn get_edge_chunking_context_with_client_assets( environment.to_resolved().await?, next_mode.runtime_type(), ) - .asset_base_path(asset_prefix.owned().await?) + .asset_base_path(asset_prefix) .minify_type(if *turbo_minify.await? { MinifyType::Minify { // React needs deterministic function names to work correctly. @@ -279,6 +279,8 @@ pub async fn get_edge_chunking_context_with_client_assets( #[turbo_tasks::function] pub async fn get_edge_chunking_context( options: EdgeChunkingContextOptions, + client_root: FileSystemPath, + asset_prefix: Option, ) -> Result>> { let EdgeChunkingContextOptions { mode, @@ -305,6 +307,9 @@ pub async fn get_edge_chunking_context( environment.to_resolved().await?, next_mode.runtime_type(), ) + .client_roots_override(rcstr!("client"), client_root.clone()) + .asset_root_path_override(rcstr!("client"), client_root.join("static/media")?) + .asset_base_path_override(rcstr!("client"), asset_prefix.unwrap()) // Since one can't read files in edge directly, any asset need to be fetched // instead. This special blob url is handled by the custom fetch // implementation in the edge sandbox. It will respond with the diff --git a/crates/next-core/src/next_image/module.rs b/crates/next-core/src/next_image/module.rs index 286748d7336b83..81c0ad1f2e310c 100644 --- a/crates/next-core/src/next_image/module.rs +++ b/crates/next-core/src/next_image/module.rs @@ -56,7 +56,9 @@ impl StructuredImageModuleType { blur_placeholder_mode: BlurPlaceholderMode, module_asset_context: ResolvedVc, ) -> Result>> { - let static_asset = StaticUrlJsModule::new(*source).to_resolved().await?; + let static_asset = StaticUrlJsModule::new(*source, Some(rcstr!("client"))) + .to_resolved() + .await?; Ok(module_asset_context .process( Vc::upcast( diff --git a/crates/next-core/src/next_server/context.rs b/crates/next-core/src/next_server/context.rs index 0d25bd170ee444..8628016ff52f01 100644 --- a/crates/next-core/src/next_server/context.rs +++ b/crates/next-core/src/next_server/context.rs @@ -1079,6 +1079,8 @@ pub async fn get_server_chunking_context_with_client_assets( #[turbo_tasks::function] pub async fn get_server_chunking_context( options: ServerChunkingContextOptions, + client_root: FileSystemPath, + asset_prefix: Option, ) -> Result> { let ServerChunkingContextOptions { mode, @@ -1108,6 +1110,9 @@ pub async fn get_server_chunking_context( environment.to_resolved().await?, next_mode.runtime_type(), ) + .client_roots_override(rcstr!("client"), client_root.clone()) + .asset_root_path_override(rcstr!("client"), client_root.join("static/media")?) + .asset_prefix_override(rcstr!("client"), asset_prefix.unwrap()) .minify_type(if *turbo_minify.await? { MinifyType::Minify { mangle: (!*no_mangling.await?).then_some(MangleType::OptimalSize), diff --git a/turbopack/crates/turbopack-browser/src/chunking_context.rs b/turbopack/crates/turbopack-browser/src/chunking_context.rs index f8903e058525b6..2346c085b5038f 100644 --- a/turbopack/crates/turbopack-browser/src/chunking_context.rs +++ b/turbopack/crates/turbopack-browser/src/chunking_context.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use tracing::Instrument; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ - NonLocalValue, ResolvedVc, TaskInput, TryJoinIterExt, Upcast, ValueToString, Vc, + FxIndexMap, NonLocalValue, ResolvedVc, TaskInput, TryJoinIterExt, Upcast, ValueToString, Vc, trace::TraceRawVcs, }; use turbo_tasks_fs::FileSystemPath; @@ -173,6 +173,21 @@ impl BrowserChunkingContextBuilder { self } + pub fn asset_root_path_override(mut self, tag: RcStr, path: FileSystemPath) -> Self { + self.chunking_context.asset_root_paths.insert(tag, path); + self + } + + pub fn client_roots_override(mut self, tag: RcStr, path: FileSystemPath) -> Self { + self.chunking_context.client_roots.insert(tag, path); + self + } + + pub fn asset_base_path_override(mut self, tag: RcStr, path: RcStr) -> Self { + self.chunking_context.asset_base_paths.insert(tag, path); + self + } + pub fn chunking_config(mut self, ty: ResolvedVc, chunking_config: ChunkingConfig) -> Self where T: Upcast>, @@ -200,7 +215,7 @@ impl BrowserChunkingContextBuilder { /// It splits "node_modules" separately as these are less likely to change /// during development #[turbo_tasks::value] -#[derive(Debug, Clone, Hash, TaskInput)] +#[derive(Debug, Clone)] pub struct BrowserChunkingContext { name: Option, /// The root path of the project @@ -213,10 +228,14 @@ pub struct BrowserChunkingContext { output_root_to_root_path: RcStr, /// This path is used to compute the url to request assets from client_root: FileSystemPath, + /// This path is used to compute the url to request chunks or assets from + client_roots: FxIndexMap, /// Chunks are placed at this path chunk_root_path: FileSystemPath, /// Static assets are placed at this path asset_root_path: FileSystemPath, + /// Static assets are placed at this path + asset_root_paths: FxIndexMap, /// Base path that will be prepended to all chunk URLs when loading them. /// This path will not appear in chunk paths or chunk data. chunk_base_path: Option, @@ -226,6 +245,9 @@ pub struct BrowserChunkingContext { /// URL prefix that will be prepended to all static asset URLs when loading /// them. asset_base_path: Option, + /// URL prefix that will be prepended to all static asset URLs when loading + /// them. + asset_base_paths: FxIndexMap, /// Enable HMR for this chunking enable_hot_module_replacement: bool, /// Enable tracing for this chunking @@ -276,12 +298,15 @@ impl BrowserChunkingContext { output_root, output_root_to_root_path, client_root, + client_roots: Default::default(), chunk_root_path, should_use_file_source_map_uris: false, asset_root_path, + asset_root_paths: Default::default(), chunk_base_path: None, chunk_suffix_path: None, asset_base_path: None, + asset_base_paths: Default::default(), enable_hot_module_replacement: false, enable_tracing: false, enable_module_merging: false, @@ -497,19 +522,30 @@ impl ChunkingContext for BrowserChunkingContext { } #[turbo_tasks::function] - async fn asset_url(&self, ident: FileSystemPath) -> Result> { + async fn asset_url(&self, ident: FileSystemPath, tag: Option) -> Result> { let asset_path = ident.to_string(); + + let client_root = if let Some(p) = tag.as_ref().and_then(|tag| self.client_roots.get(tag)) { + p + } else { + &self.client_root + }; + + let asset_base_path = + if let Some(p) = tag.as_ref().and_then(|tag| self.asset_base_paths.get(tag)) { + Some(p) + } else { + self.asset_base_path.as_ref() + }; + let asset_path = asset_path - .strip_prefix(&format!("{}/", self.client_root.path)) + .strip_prefix(&format!("{}/", client_root.path)) .context("expected asset_path to contain client_root")?; Ok(Vc::cell( format!( "{}{}", - self.asset_base_path - .as_ref() - .map(|s| s.as_str()) - .unwrap_or("/"), + asset_base_path.map(|s| s.as_str()).unwrap_or("/"), asset_path ) .into(), @@ -537,6 +573,7 @@ impl ChunkingContext for BrowserChunkingContext { &self, content_hash: RcStr, original_asset_ident: Vc, + tag: Option, ) -> Result> { let source_path = original_asset_ident.path().await?; let basename = source_path.file_name(); @@ -551,7 +588,15 @@ impl ChunkingContext for BrowserChunkingContext { content_hash = &content_hash[..8] ), }; - Ok(self.asset_root_path.join(&asset_path)?.cell()) + + let asset_root_path = + if let Some(p) = tag.as_ref().and_then(|tag| self.asset_root_paths.get(tag)) { + p + } else { + &self.asset_root_path + }; + + Ok(asset_root_path.join(&asset_path)?.cell()) } #[turbo_tasks::function] diff --git a/turbopack/crates/turbopack-core/src/chunk/chunking_context.rs b/turbopack/crates/turbopack-core/src/chunk/chunking_context.rs index 519b1cb1bb66f4..2d7b3d56761835 100644 --- a/turbopack/crates/turbopack-core/src/chunk/chunking_context.rs +++ b/turbopack/crates/turbopack-core/src/chunk/chunking_context.rs @@ -185,13 +185,14 @@ pub trait ChunkingContext { /// Returns a URL (relative or absolute, depending on the asset prefix) to /// the static asset based on its `ident`. #[turbo_tasks::function] - fn asset_url(self: Vc, ident: FileSystemPath) -> Result>; + fn asset_url(self: Vc, ident: FileSystemPath, tag: Option) -> Result>; #[turbo_tasks::function] fn asset_path( self: Vc, content_hash: RcStr, original_asset_ident: Vc, + tag: Option, ) -> Vc; #[turbo_tasks::function] diff --git a/turbopack/crates/turbopack-nodejs/src/chunking_context.rs b/turbopack/crates/turbopack-nodejs/src/chunking_context.rs index e19d1dcb7f8006..20c9834f00378c 100644 --- a/turbopack/crates/turbopack-nodejs/src/chunking_context.rs +++ b/turbopack/crates/turbopack-nodejs/src/chunking_context.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result, bail}; use tracing::Instrument; use turbo_rcstr::{RcStr, rcstr}; -use turbo_tasks::{ResolvedVc, TaskInput, TryJoinIterExt, Upcast, ValueToString, Vc}; +use turbo_tasks::{FxIndexMap, ResolvedVc, TryJoinIterExt, Upcast, ValueToString, Vc}; use turbo_tasks_fs::FileSystemPath; use turbopack_core::{ asset::Asset, @@ -45,6 +45,21 @@ impl NodeJsChunkingContextBuilder { self } + pub fn asset_prefix_override(mut self, tag: RcStr, prefix: RcStr) -> Self { + self.chunking_context.asset_prefixes.insert(tag, prefix); + self + } + + pub fn asset_root_path_override(mut self, tag: RcStr, path: FileSystemPath) -> Self { + self.chunking_context.asset_root_paths.insert(tag, path); + self + } + + pub fn client_roots_override(mut self, tag: RcStr, path: FileSystemPath) -> Self { + self.chunking_context.client_roots.insert(tag, path); + self + } + pub fn minify_type(mut self, minify_type: MinifyType) -> Self { self.chunking_context.minify_type = minify_type; self @@ -125,7 +140,7 @@ impl NodeJsChunkingContextBuilder { /// A chunking context for build mode. #[turbo_tasks::value] -#[derive(Debug, Clone, Hash, TaskInput)] +#[derive(Debug, Clone)] pub struct NodeJsChunkingContext { /// The root path of the project root_path: FileSystemPath, @@ -135,12 +150,18 @@ pub struct NodeJsChunkingContext { output_root_to_root_path: RcStr, /// This path is used to compute the url to request chunks or assets from client_root: FileSystemPath, + /// This path is used to compute the url to request chunks or assets from + client_roots: FxIndexMap, /// Chunks are placed at this path chunk_root_path: FileSystemPath, /// Static assets are placed at this path asset_root_path: FileSystemPath, + /// Static assets are placed at this path + asset_root_paths: FxIndexMap, /// Static assets requested from this url base asset_prefix: Option, + /// Static assets requested from this url base + asset_prefixes: FxIndexMap, /// The environment chunks will be evaluated in. environment: ResolvedVc, /// The kind of runtime to include in the output. @@ -187,9 +208,12 @@ impl NodeJsChunkingContext { output_root, output_root_to_root_path, client_root, + client_roots: Default::default(), chunk_root_path, asset_root_path, + asset_root_paths: Default::default(), asset_prefix: None, + asset_prefixes: Default::default(), enable_file_tracing: false, enable_module_merging: false, enable_dynamic_chunk_content_loading: false, @@ -298,16 +322,30 @@ impl ChunkingContext for NodeJsChunkingContext { } #[turbo_tasks::function] - async fn asset_url(&self, ident: FileSystemPath) -> Result> { + async fn asset_url(&self, ident: FileSystemPath, tag: Option) -> Result> { let asset_path = ident.to_string(); + + let client_root = if let Some(p) = tag.as_ref().and_then(|tag| self.client_roots.get(tag)) { + p + } else { + &self.client_root + }; + + let asset_prefix = + if let Some(p) = tag.as_ref().and_then(|tag| self.asset_prefixes.get(tag)) { + Some(p) + } else { + self.asset_prefix.as_ref() + }; + let asset_path = asset_path - .strip_prefix(&format!("{}/", self.client_root.path)) + .strip_prefix(&format!("{}/", client_root.path)) .context("expected client root to contain asset path")?; Ok(Vc::cell( format!( "{}{}", - self.asset_prefix.clone().unwrap_or(rcstr!("/")), + asset_prefix.map(|s| s.as_str()).unwrap_or("/"), asset_path ) .into(), @@ -366,6 +404,7 @@ impl ChunkingContext for NodeJsChunkingContext { &self, content_hash: RcStr, original_asset_ident: Vc, + tag: Option, ) -> Result> { let source_path = original_asset_ident.path().await?; let basename = source_path.file_name(); @@ -380,7 +419,15 @@ impl ChunkingContext for NodeJsChunkingContext { content_hash = &content_hash[..8] ), }; - Ok(self.asset_root_path.join(&asset_path)?.cell()) + + let asset_root_path = + if let Some(p) = tag.as_ref().and_then(|tag| self.asset_root_paths.get(tag)) { + p + } else { + &self.asset_root_path + }; + + Ok(asset_root_path.join(&asset_path)?.cell()) } #[turbo_tasks::function] diff --git a/turbopack/crates/turbopack-static/src/css.rs b/turbopack/crates/turbopack-static/src/css.rs index 04f2724edcc9ad..b689eedada1b69 100644 --- a/turbopack/crates/turbopack-static/src/css.rs +++ b/turbopack/crates/turbopack-static/src/css.rs @@ -1,4 +1,4 @@ -use turbo_rcstr::rcstr; +use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ResolvedVc, Vc}; use turbopack_core::{ asset::{Asset, AssetContent}, @@ -16,13 +16,14 @@ use crate::output_asset::StaticOutputAsset; #[derive(Clone)] pub struct StaticUrlCssModule { pub source: ResolvedVc>, + tag: Option, } #[turbo_tasks::value_impl] impl StaticUrlCssModule { #[turbo_tasks::function] - pub fn new(source: ResolvedVc>) -> Vc { - Self::cell(StaticUrlCssModule { source }) + pub fn new(source: ResolvedVc>, tag: Option) -> Vc { + Self::cell(StaticUrlCssModule { source, tag }) } #[turbo_tasks::function] @@ -30,7 +31,7 @@ impl StaticUrlCssModule { &self, chunking_context: ResolvedVc>, ) -> Vc { - StaticOutputAsset::new(*chunking_context, *self.source) + StaticOutputAsset::new(*chunking_context, *self.source, self.tag.clone()) } } diff --git a/turbopack/crates/turbopack-static/src/ecma.rs b/turbopack/crates/turbopack-static/src/ecma.rs index e901981595e674..cf5530fdfacb8e 100644 --- a/turbopack/crates/turbopack-static/src/ecma.rs +++ b/turbopack/crates/turbopack-static/src/ecma.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use turbo_rcstr::rcstr; +use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ResolvedVc, Vc}; use turbopack_core::{ asset::{Asset, AssetContent}, @@ -25,13 +25,14 @@ use crate::output_asset::StaticOutputAsset; #[derive(Clone)] pub struct StaticUrlJsModule { pub source: ResolvedVc>, + pub tag: Option, } #[turbo_tasks::value_impl] impl StaticUrlJsModule { #[turbo_tasks::function] - pub fn new(source: ResolvedVc>) -> Vc { - Self::cell(StaticUrlJsModule { source }) + pub fn new(source: ResolvedVc>, tag: Option) -> Vc { + Self::cell(StaticUrlJsModule { source, tag }) } #[turbo_tasks::function] @@ -39,7 +40,7 @@ impl StaticUrlJsModule { &self, chunking_context: ResolvedVc>, ) -> Vc { - StaticOutputAsset::new(*chunking_context, *self.source) + StaticOutputAsset::new(*chunking_context, *self.source, self.tag.clone()) } } @@ -47,9 +48,14 @@ impl StaticUrlJsModule { impl Module for StaticUrlJsModule { #[turbo_tasks::function] fn ident(&self) -> Vc { - self.source + let mut ident = self + .source .ident() - .with_modifier(rcstr!("static in ecmascript")) + .with_modifier(rcstr!("static in ecmascript")); + if let Some(tag) = &self.tag { + ident = ident.with_modifier(format!("tag {}", tag).into()); + } + ident } } @@ -77,6 +83,7 @@ impl ChunkableModule for StaticUrlJsModule { .static_output_asset(*chunking_context) .to_resolved() .await?, + tag: self.await?.tag.clone(), }, ))) } @@ -95,6 +102,7 @@ struct StaticUrlJsChunkItem { module: ResolvedVc, chunking_context: ResolvedVc>, static_asset: ResolvedVc, + tag: Option, } #[turbo_tasks::value_impl] @@ -137,7 +145,7 @@ impl EcmascriptChunkItem for StaticUrlJsChunkItem { path = StringifyJs( &self .chunking_context - .asset_url(self.static_asset.path().owned().await?) + .asset_url(self.static_asset.path().owned().await?, self.tag.clone()) .await? ) ) diff --git a/turbopack/crates/turbopack-static/src/output_asset.rs b/turbopack/crates/turbopack-static/src/output_asset.rs index c1c4b868c26046..3d9600f5e078a7 100644 --- a/turbopack/crates/turbopack-static/src/output_asset.rs +++ b/turbopack/crates/turbopack-static/src/output_asset.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use turbo_rcstr::RcStr; use turbo_tasks::{ResolvedVc, Vc}; use turbo_tasks_fs::{FileContent, FileSystemPath}; use turbopack_core::{ @@ -11,6 +12,7 @@ use turbopack_core::{ pub struct StaticOutputAsset { chunking_context: ResolvedVc>, source: ResolvedVc>, + tag: Option, } #[turbo_tasks::value_impl] @@ -19,10 +21,12 @@ impl StaticOutputAsset { pub fn new( chunking_context: ResolvedVc>, source: ResolvedVc>, + tag: Option, ) -> Vc { Self::cell(StaticOutputAsset { chunking_context, source, + tag, }) } } @@ -42,9 +46,11 @@ impl OutputAsset for StaticOutputAsset { anyhow::bail!("StaticAsset::path: unsupported file content") }; let content_hash_b16 = turbo_tasks_hash::encode_hex(content_hash); - Ok(self - .chunking_context - .asset_path(content_hash_b16.into(), self.source.ident())) + Ok(self.chunking_context.asset_path( + content_hash_b16.into(), + self.source.ident(), + self.tag.clone(), + )) } } diff --git a/turbopack/crates/turbopack/src/lib.rs b/turbopack/crates/turbopack/src/lib.rs index 24fc0d504df7b7..b3f89d2e7589da 100644 --- a/turbopack/crates/turbopack/src/lib.rs +++ b/turbopack/crates/turbopack/src/lib.rs @@ -271,11 +271,14 @@ async fn apply_module_type( .to_resolved() .await?, ), - ModuleType::StaticUrlJs => { - ResolvedVc::upcast(StaticUrlJsModule::new(*source).to_resolved().await?) - } + ModuleType::StaticUrlJs { tag } => ResolvedVc::upcast( + StaticUrlJsModule::new(*source, tag.clone()) + .to_resolved() + .await?, + ), ModuleType::StaticUrlCss => { - ResolvedVc::upcast(StaticUrlCssModule::new(*source).to_resolved().await?) + // TODO + ResolvedVc::upcast(StaticUrlCssModule::new(*source, None).to_resolved().await?) } ModuleType::InlinedBytesJs => { ResolvedVc::upcast(InlinedBytesJsModule::new(*source).to_resolved().await?) diff --git a/turbopack/crates/turbopack/src/module_options/mod.rs b/turbopack/crates/turbopack/src/module_options/mod.rs index 2de0143c34ff53..d9b55302e96be1 100644 --- a/turbopack/crates/turbopack/src/module_options/mod.rs +++ b/turbopack/crates/turbopack/src/module_options/mod.rs @@ -434,11 +434,23 @@ impl ModuleOptions { RuleCondition::ResourcePathEndsWith(".webp".to_string()), RuleCondition::ResourcePathEndsWith(".woff2".to_string()), ]), - vec![ModuleRuleEffect::ModuleType(ModuleType::StaticUrlJs)], + vec![ModuleRuleEffect::ModuleType(ModuleType::StaticUrlJs { + tag: None, + })], ), ModuleRule::new( RuleCondition::ReferenceType(ReferenceType::Url(UrlReferenceSubType::Undefined)), - vec![ModuleRuleEffect::ModuleType(ModuleType::StaticUrlJs)], + vec![ModuleRuleEffect::ModuleType(ModuleType::StaticUrlJs { + tag: None, + })], + ), + ModuleRule::new( + RuleCondition::ReferenceType(ReferenceType::Url( + UrlReferenceSubType::EcmaScriptNewUrl, + )), + vec![ModuleRuleEffect::ModuleType(ModuleType::StaticUrlJs { + tag: None, + })], ), ModuleRule::new( RuleCondition::ReferenceType(ReferenceType::Url(UrlReferenceSubType::CssUrl)), diff --git a/turbopack/crates/turbopack/src/module_options/module_rule.rs b/turbopack/crates/turbopack/src/module_options/module_rule.rs index f1e017c1d3ee19..dfbb5be4a2df43 100644 --- a/turbopack/crates/turbopack/src/module_options/module_rule.rs +++ b/turbopack/crates/turbopack/src/module_options/module_rule.rs @@ -1,5 +1,6 @@ use anyhow::Result; use serde::{Deserialize, Serialize}; +use turbo_rcstr::RcStr; use turbo_tasks::{NonLocalValue, ResolvedVc, trace::TraceRawVcs}; use turbo_tasks_fs::FileSystemPath; use turbopack_core::{ @@ -138,7 +139,9 @@ pub enum ModuleType { ty: CssModuleAssetType, environment: Option>, }, - StaticUrlJs, + StaticUrlJs { + tag: Option, + }, StaticUrlCss, InlinedBytesJs, WebAssembly { From 7e70e104cb3ed9d47ff527ff05a05288ba3ebbb0 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:34:09 +0200 Subject: [PATCH 04/10] f test --- test/e2e/url/url.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/e2e/url/url.test.ts b/test/e2e/url/url.test.ts index eb5cd5726bfcff..1ce00b6fd169c0 100644 --- a/test/e2e/url/url.test.ts +++ b/test/e2e/url/url.test.ts @@ -47,9 +47,14 @@ describe(`Handle new URL asset references`, () => { sizes: '512x512', }, ], - description: expect.stringMatching( - /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ - ), + // TODO Webpack bug? + description: process.env.IS_TURBOPACK_TEST + ? expect.stringMatching( + /file:.*\/.next\/server\/.*\/vercel\.[0-9a-f]{8}\.png$/ + ) + : expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ), }) }) From 686173c5a44b30e8220efa47608c0b788ffa9d43 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:38:47 +0200 Subject: [PATCH 05/10] f test --- test/e2e/url/url.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/e2e/url/url.test.ts b/test/e2e/url/url.test.ts index 1ce00b6fd169c0..3258f00608ec85 100644 --- a/test/e2e/url/url.test.ts +++ b/test/e2e/url/url.test.ts @@ -1,11 +1,14 @@ import { retry } from 'next-test-utils' import { nextTestSetup } from 'e2e-utils' -// | | Pages Client | Pages Server (SSR,RSC) | API Routes/Middleware | Metadata Routes | -// |---------|-------------------------|-------------------------|-------------------------|-------------------------| -// | new URL | /_next/static/media/... | /_next/static/media/... | /server/assets/... | /_next/static/media/... | -// | import | /_next/static/media/... | /_next/static/media/... | /_next/static/media/... | /_next/static/media/... | -// |---------|-------------------------|-------------------------|-------------------------|-------------------------| +// | | Pages Client | Pages Server (SSR,RSC) | API Routes/Middleware/Metadata | +// |---------|-------------------------|-------------------------|--------------------------------| +// | new URL | /_next/static/media/... | /_next/static/media/... | /server/assets/... | +// | import | /_next/static/media/... | /_next/static/media/... | /_next/static/media/... | +// |---------|-------------------------|-------------------------|--------------------------------| +// +// Webpack has a (potential?) bug that App Router API routes (and Metadata) return client assets for +// `new URL`s. describe(`Handle new URL asset references`, () => { const { next } = nextTestSetup({ files: __dirname, From 50f7027c8cc10e152c9327f70d0ade173477c5a3 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:34:04 +0200 Subject: [PATCH 06/10] test edge --- test/e2e/url/app/api/edge/route.js | 8 +++++++ test/e2e/url/app/client-edge/page.js | 14 ++++++++++++ test/e2e/url/app/rsc-edge/page.js | 12 +++++++++++ test/e2e/url/pages/api/pages-edge/index.js | 13 +++++++++++ test/e2e/url/pages/api/pages/index.js | 2 +- test/e2e/url/pages/pages-edge/ssr.js | 20 +++++++++++++++++ test/e2e/url/pages/pages-edge/static.js | 12 +++++++++++ test/e2e/url/url.test.ts | 25 ++++++++++++++++++++-- 8 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 test/e2e/url/app/api/edge/route.js create mode 100644 test/e2e/url/app/client-edge/page.js create mode 100644 test/e2e/url/app/rsc-edge/page.js create mode 100644 test/e2e/url/pages/api/pages-edge/index.js create mode 100644 test/e2e/url/pages/pages-edge/ssr.js create mode 100644 test/e2e/url/pages/pages-edge/static.js diff --git a/test/e2e/url/app/api/edge/route.js b/test/e2e/url/app/api/edge/route.js new file mode 100644 index 00000000000000..c105ed9d4e01d7 --- /dev/null +++ b/test/e2e/url/app/api/edge/route.js @@ -0,0 +1,8 @@ +import imported from '../../../public/vercel.png' +const url = new URL('../../../public/vercel.png', import.meta.url).toString() + +export function GET(req, res) { + return Response.json({ imported, url }) +} + +export const runtime = 'edge' diff --git a/test/e2e/url/app/client-edge/page.js b/test/e2e/url/app/client-edge/page.js new file mode 100644 index 00000000000000..8b5aea8a1e9672 --- /dev/null +++ b/test/e2e/url/app/client-edge/page.js @@ -0,0 +1,14 @@ +'use client' + +import imported from '../../public/vercel.png' +const url = new URL('../../public/vercel.png', import.meta.url).toString() + +export default function Index(props) { + return ( +
+ Hello {imported.src}+{url} +
+ ) +} + +export const runtime = 'edge' diff --git a/test/e2e/url/app/rsc-edge/page.js b/test/e2e/url/app/rsc-edge/page.js new file mode 100644 index 00000000000000..b673e2eb62fdcb --- /dev/null +++ b/test/e2e/url/app/rsc-edge/page.js @@ -0,0 +1,12 @@ +import imported from '../../public/vercel.png' +const url = new URL('../../public/vercel.png', import.meta.url).toString() + +export default function Index(props) { + return ( +
+ Hello {imported.src}+{url} +
+ ) +} + +export const runtime = 'edge' diff --git a/test/e2e/url/pages/api/pages-edge/index.js b/test/e2e/url/pages/api/pages-edge/index.js new file mode 100644 index 00000000000000..d124c191ac55ef --- /dev/null +++ b/test/e2e/url/pages/api/pages-edge/index.js @@ -0,0 +1,13 @@ +import imported from '../../../public/vercel.png' +const url = new URL('../../../public/vercel.png', import.meta.url) + +export default (req, res) => { + return new Response( + JSON.stringify({ + imported, + url: url.toString(), + }) + ) +} + +export const runtime = 'experimental-edge' diff --git a/test/e2e/url/pages/api/pages/index.js b/test/e2e/url/pages/api/pages/index.js index 17347a6eaa50d1..4da25e8db01da3 100644 --- a/test/e2e/url/pages/api/pages/index.js +++ b/test/e2e/url/pages/api/pages/index.js @@ -4,7 +4,7 @@ import imported from '../../../public/vercel.png' const url = new URL('../../../public/vercel.png', import.meta.url) export default (req, res) => { - res.json({ + res.send({ imported, url: url.toString(), size: fs.readFileSync(url).length, diff --git a/test/e2e/url/pages/pages-edge/ssr.js b/test/e2e/url/pages/pages-edge/ssr.js new file mode 100644 index 00000000000000..de5792607d3e7f --- /dev/null +++ b/test/e2e/url/pages/pages-edge/ssr.js @@ -0,0 +1,20 @@ +import imported from '../../public/vercel.png' + +export function getServerSideProps() { + return { + props: { + url: new URL('../../public/vercel.png', import.meta.url).toString(), + }, + } +} + +export default function Index({ url }) { + return ( +
+ Hello {imported.src}+ + {new URL('../../public/vercel.png', import.meta.url).toString()}+{url} +
+ ) +} + +export const runtime = 'experimental-edge' diff --git a/test/e2e/url/pages/pages-edge/static.js b/test/e2e/url/pages/pages-edge/static.js new file mode 100644 index 00000000000000..de324501b4d9b0 --- /dev/null +++ b/test/e2e/url/pages/pages-edge/static.js @@ -0,0 +1,12 @@ +import imported from '../../public/vercel.png' +const url = new URL('../../public/vercel.png', import.meta.url).toString() + +export default function Index(props) { + return ( +
+ Hello {imported.src}+{url} +
+ ) +} + +export const runtime = 'experimental-edge' diff --git a/test/e2e/url/url.test.ts b/test/e2e/url/url.test.ts index 3258f00608ec85..3187282f63e11a 100644 --- a/test/e2e/url/url.test.ts +++ b/test/e2e/url/url.test.ts @@ -61,7 +61,7 @@ describe(`Handle new URL asset references`, () => { }) }) - for (const page of ['/rsc', '/client']) { + for (const page of ['/rsc', '/rsc-edge', '/client', '/client-edge']) { it(`should render the ${page} page`, async () => { const $ = await next.render$(page) expect($('main').text()).toMatch(expectedPage) @@ -99,7 +99,13 @@ describe(`Handle new URL asset references`, () => { }) describe('pages router', () => { - for (const page of ['/pages/static', '/pages/ssr', '/pages/ssg']) { + for (const page of [ + '/pages/static', + '/pages/ssr', + '/pages/ssg', + '/pages-edge/static', + '/pages-edge/ssr', + ]) { it(`should render the ${page} page`, async () => { const $ = await next.render$(page) expect($('main').text()).toMatch(expectedPage) @@ -132,5 +138,20 @@ describe(`Handle new URL asset references`, () => { size: 30079, }) }) + + it('should respond on edge API', async () => { + const data = await next + .fetch('/api/pages-edge/') + .then((res) => res.ok && res.json()) + + expect(data).toEqual({ + imported: expect.objectContaining({ + src: expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ), + }), + url: expect.stringMatching(/^blob:.*vercel\.[0-9a-f]{8,}\.png$/), + }) + }) }) }) From 35b3de09d3ba681afab3165af35acc665ece368a Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 9 Oct 2025 18:02:35 +0200 Subject: [PATCH 07/10] f test --- test/e2e/url/url.test.ts | 68 ++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/test/e2e/url/url.test.ts b/test/e2e/url/url.test.ts index 3187282f63e11a..3613459dc2cc16 100644 --- a/test/e2e/url/url.test.ts +++ b/test/e2e/url/url.test.ts @@ -62,19 +62,29 @@ describe(`Handle new URL asset references`, () => { }) for (const page of ['/rsc', '/rsc-edge', '/client', '/client-edge']) { - it(`should render the ${page} page`, async () => { - const $ = await next.render$(page) - expect($('main').text()).toMatch(expectedPage) - }) - - it(`should client-render the ${page} page`, async () => { - const browser = await next.browser(page) - await retry(async () => - expect(await browser.elementByCss('main').text()).toMatch( - expectedPage + let shouldSkip = process.env.IS_TURBOPACK_TEST + ? false + : page.includes('edge') + + ;(shouldSkip ? it.skip : it)( + `should render the ${page} page`, + async () => { + const $ = await next.render$(page) + // eslint-disable-next-line jest/no-standalone-expect + expect($('main').text()).toMatch(expectedPage) + } + ) + ;(shouldSkip ? it.skip : it)( + `should client-render the ${page} page`, + async () => { + const browser = await next.browser(page) + await retry(async () => + expect(await browser.elementByCss('main').text()).toMatch( + expectedPage + ) ) - ) - }) + } + ) } it('should respond on API', async () => { @@ -106,19 +116,29 @@ describe(`Handle new URL asset references`, () => { '/pages-edge/static', '/pages-edge/ssr', ]) { - it(`should render the ${page} page`, async () => { - const $ = await next.render$(page) - expect($('main').text()).toMatch(expectedPage) - }) - - it(`should client-render the ${page} page`, async () => { - const browser = await next.browser(page) - await retry(async () => - expect(await browser.elementByCss('main').text()).toMatch( - expectedPage + let shouldSkip = process.env.IS_TURBOPACK_TEST + ? false + : page.includes('edge') + + ;(shouldSkip ? it.skip : it)( + `should render the ${page} page`, + async () => { + const $ = await next.render$(page) + // eslint-disable-next-line jest/no-standalone-expect + expect($('main').text()).toMatch(expectedPage) + } + ) + ;(shouldSkip ? it.skip : it)( + `should client-render the ${page} page`, + async () => { + const browser = await next.browser(page) + await retry(async () => + expect(await browser.elementByCss('main').text()).toMatch( + expectedPage + ) ) - ) - }) + } + ) } it('should respond on API', async () => { From 44664409e1e3edc08315615b7b6be9756172eb8f Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:47:46 +0200 Subject: [PATCH 08/10] test opengraph-image --- test/e2e/url/app/opengraph-image.js | 9 +++++++++ test/e2e/url/url.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 test/e2e/url/app/opengraph-image.js diff --git a/test/e2e/url/app/opengraph-image.js b/test/e2e/url/app/opengraph-image.js new file mode 100644 index 00000000000000..07cd809ad1b0fb --- /dev/null +++ b/test/e2e/url/app/opengraph-image.js @@ -0,0 +1,9 @@ +import imported from '../public/vercel.png' +const url = new URL('../public/vercel.png', import.meta.url).toString() + +export const contentType = 'text/json' + +// Image generation +export default async function Image() { + return Response.json({ imported, url }) +} diff --git a/test/e2e/url/url.test.ts b/test/e2e/url/url.test.ts index 3613459dc2cc16..5382bae5f044e2 100644 --- a/test/e2e/url/url.test.ts +++ b/test/e2e/url/url.test.ts @@ -61,6 +61,28 @@ describe(`Handle new URL asset references`, () => { }) }) + it('should respond on opengraph-image', async () => { + const data = await next + .fetch('/opengraph-image') + .then((res) => res.ok && res.json()) + + expect(data).toEqual({ + imported: expect.objectContaining({ + src: expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ), + }), + // TODO Webpack bug? + url: process.env.IS_TURBOPACK_TEST + ? expect.stringMatching( + /file:.*\/.next\/server\/.*\/vercel\.[0-9a-f]{8}\.png$/ + ) + : expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ), + }) + }) + for (const page of ['/rsc', '/rsc-edge', '/client', '/client-edge']) { let shouldSkip = process.env.IS_TURBOPACK_TEST ? false From efac4eb3a79e6ba6b1748fa2061a1313b1cf0eba Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:56:49 +0200 Subject: [PATCH 09/10] cleanup --- test/e2e/url/url.test.ts | 2 ++ .../turbopack-browser/src/chunking_context.rs | 29 ++++++++----------- .../turbopack-nodejs/src/chunking_context.rs | 29 ++++++++----------- turbopack/crates/turbopack/src/lib.rs | 9 +++--- .../turbopack/src/module_options/mod.rs | 4 ++- .../src/module_options/module_rule.rs | 4 ++- 6 files changed, 37 insertions(+), 40 deletions(-) diff --git a/test/e2e/url/url.test.ts b/test/e2e/url/url.test.ts index 5382bae5f044e2..296c5cc026bc2d 100644 --- a/test/e2e/url/url.test.ts +++ b/test/e2e/url/url.test.ts @@ -84,6 +84,7 @@ describe(`Handle new URL asset references`, () => { }) for (const page of ['/rsc', '/rsc-edge', '/client', '/client-edge']) { + // TODO Webpack bug? let shouldSkip = process.env.IS_TURBOPACK_TEST ? false : page.includes('edge') @@ -138,6 +139,7 @@ describe(`Handle new URL asset references`, () => { '/pages-edge/static', '/pages-edge/ssr', ]) { + // TODO Webpack bug? let shouldSkip = process.env.IS_TURBOPACK_TEST ? false : page.includes('edge') diff --git a/turbopack/crates/turbopack-browser/src/chunking_context.rs b/turbopack/crates/turbopack-browser/src/chunking_context.rs index 2346c085b5038f..dcdf2158154b78 100644 --- a/turbopack/crates/turbopack-browser/src/chunking_context.rs +++ b/turbopack/crates/turbopack-browser/src/chunking_context.rs @@ -525,18 +525,15 @@ impl ChunkingContext for BrowserChunkingContext { async fn asset_url(&self, ident: FileSystemPath, tag: Option) -> Result> { let asset_path = ident.to_string(); - let client_root = if let Some(p) = tag.as_ref().and_then(|tag| self.client_roots.get(tag)) { - p - } else { - &self.client_root - }; + let client_root = tag + .as_ref() + .and_then(|tag| self.client_roots.get(tag)) + .unwrap_or(&self.client_root); - let asset_base_path = - if let Some(p) = tag.as_ref().and_then(|tag| self.asset_base_paths.get(tag)) { - Some(p) - } else { - self.asset_base_path.as_ref() - }; + let asset_base_path = tag + .as_ref() + .and_then(|tag| self.asset_base_paths.get(tag)) + .or(self.asset_base_path.as_ref()); let asset_path = asset_path .strip_prefix(&format!("{}/", client_root.path)) @@ -589,12 +586,10 @@ impl ChunkingContext for BrowserChunkingContext { ), }; - let asset_root_path = - if let Some(p) = tag.as_ref().and_then(|tag| self.asset_root_paths.get(tag)) { - p - } else { - &self.asset_root_path - }; + let asset_root_path = tag + .as_ref() + .and_then(|tag| self.asset_root_paths.get(tag)) + .unwrap_or(&self.asset_root_path); Ok(asset_root_path.join(&asset_path)?.cell()) } diff --git a/turbopack/crates/turbopack-nodejs/src/chunking_context.rs b/turbopack/crates/turbopack-nodejs/src/chunking_context.rs index 20c9834f00378c..c877f39598bee3 100644 --- a/turbopack/crates/turbopack-nodejs/src/chunking_context.rs +++ b/turbopack/crates/turbopack-nodejs/src/chunking_context.rs @@ -325,18 +325,15 @@ impl ChunkingContext for NodeJsChunkingContext { async fn asset_url(&self, ident: FileSystemPath, tag: Option) -> Result> { let asset_path = ident.to_string(); - let client_root = if let Some(p) = tag.as_ref().and_then(|tag| self.client_roots.get(tag)) { - p - } else { - &self.client_root - }; + let client_root = tag + .as_ref() + .and_then(|tag| self.client_roots.get(tag)) + .unwrap_or(&self.client_root); - let asset_prefix = - if let Some(p) = tag.as_ref().and_then(|tag| self.asset_prefixes.get(tag)) { - Some(p) - } else { - self.asset_prefix.as_ref() - }; + let asset_prefix = tag + .as_ref() + .and_then(|tag| self.asset_prefixes.get(tag)) + .or(self.asset_prefix.as_ref()); let asset_path = asset_path .strip_prefix(&format!("{}/", client_root.path)) @@ -420,12 +417,10 @@ impl ChunkingContext for NodeJsChunkingContext { ), }; - let asset_root_path = - if let Some(p) = tag.as_ref().and_then(|tag| self.asset_root_paths.get(tag)) { - p - } else { - &self.asset_root_path - }; + let asset_root_path = tag + .as_ref() + .and_then(|tag| self.asset_root_paths.get(tag)) + .unwrap_or(&self.asset_root_path); Ok(asset_root_path.join(&asset_path)?.cell()) } diff --git a/turbopack/crates/turbopack/src/lib.rs b/turbopack/crates/turbopack/src/lib.rs index b3f89d2e7589da..565fd0381c977b 100644 --- a/turbopack/crates/turbopack/src/lib.rs +++ b/turbopack/crates/turbopack/src/lib.rs @@ -276,10 +276,11 @@ async fn apply_module_type( .to_resolved() .await?, ), - ModuleType::StaticUrlCss => { - // TODO - ResolvedVc::upcast(StaticUrlCssModule::new(*source, None).to_resolved().await?) - } + ModuleType::StaticUrlCss { tag } => ResolvedVc::upcast( + StaticUrlCssModule::new(*source, tag.clone()) + .to_resolved() + .await?, + ), ModuleType::InlinedBytesJs => { ResolvedVc::upcast(InlinedBytesJsModule::new(*source).to_resolved().await?) } diff --git a/turbopack/crates/turbopack/src/module_options/mod.rs b/turbopack/crates/turbopack/src/module_options/mod.rs index d9b55302e96be1..327d87be986bfb 100644 --- a/turbopack/crates/turbopack/src/module_options/mod.rs +++ b/turbopack/crates/turbopack/src/module_options/mod.rs @@ -454,7 +454,9 @@ impl ModuleOptions { ), ModuleRule::new( RuleCondition::ReferenceType(ReferenceType::Url(UrlReferenceSubType::CssUrl)), - vec![ModuleRuleEffect::ModuleType(ModuleType::StaticUrlCss)], + vec![ModuleRuleEffect::ModuleType(ModuleType::StaticUrlCss { + tag: None, + })], ), ]; diff --git a/turbopack/crates/turbopack/src/module_options/module_rule.rs b/turbopack/crates/turbopack/src/module_options/module_rule.rs index dfbb5be4a2df43..319a6866259144 100644 --- a/turbopack/crates/turbopack/src/module_options/module_rule.rs +++ b/turbopack/crates/turbopack/src/module_options/module_rule.rs @@ -142,7 +142,9 @@ pub enum ModuleType { StaticUrlJs { tag: Option, }, - StaticUrlCss, + StaticUrlCss { + tag: Option, + }, InlinedBytesJs, WebAssembly { source_ty: WebAssemblySourceType, From bc7897c8d21df431a298d2a78c3074118e4f3af7 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:43:17 +0200 Subject: [PATCH 10/10] fixup assertion --- test/e2e/url/url.test.ts | 71 +++++++++++++++------------------------- 1 file changed, 26 insertions(+), 45 deletions(-) diff --git a/test/e2e/url/url.test.ts b/test/e2e/url/url.test.ts index 296c5cc026bc2d..3b645c2e0eccd2 100644 --- a/test/e2e/url/url.test.ts +++ b/test/e2e/url/url.test.ts @@ -7,13 +7,24 @@ import { nextTestSetup } from 'e2e-utils' // | import | /_next/static/media/... | /_next/static/media/... | /_next/static/media/... | // |---------|-------------------------|-------------------------|--------------------------------| // -// Webpack has a (potential?) bug that App Router API routes (and Metadata) return client assets for -// `new URL`s. +// Webpack has +// - a bug where App Router API routes (and Metadata) return client assets for `new URL`s. +// - a bug where Edge Page routes return client assets for `new URL`s. describe(`Handle new URL asset references`, () => { const { next } = nextTestSetup({ files: __dirname, }) + const serverFilePath = expect.stringMatching( + /file:.*\/.next(\/dev)?\/server\/.*\/vercel\.[0-9a-f]{8}\.png$/ + ) + const serverEdgeUrl = expect.stringMatching( + /^blob:.*vercel\.[0-9a-f]{8,}\.png$/ + ) + const clientFilePath = expect.stringMatching( + /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ + ) + it('should respond on middleware api', async () => { const data = await next .fetch('/middleware') @@ -21,11 +32,9 @@ describe(`Handle new URL asset references`, () => { expect(data).toEqual({ imported: expect.objectContaining({ - src: expect.stringMatching( - /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ - ), + src: clientFilePath, }), - url: expect.stringMatching(/^blob:.*vercel\.[0-9a-f]{8,}\.png$/), + url: serverEdgeUrl, }) }) @@ -43,21 +52,15 @@ describe(`Handle new URL asset references`, () => { name: 'Next.js', icons: [ { - src: expect.stringMatching( - /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ - ), + src: clientFilePath, type: 'image/png', sizes: '512x512', }, ], // TODO Webpack bug? description: process.env.IS_TURBOPACK_TEST - ? expect.stringMatching( - /file:.*\/.next\/server\/.*\/vercel\.[0-9a-f]{8}\.png$/ - ) - : expect.stringMatching( - /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ - ), + ? serverFilePath + : clientFilePath, }) }) @@ -68,18 +71,10 @@ describe(`Handle new URL asset references`, () => { expect(data).toEqual({ imported: expect.objectContaining({ - src: expect.stringMatching( - /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ - ), + src: clientFilePath, }), // TODO Webpack bug? - url: process.env.IS_TURBOPACK_TEST - ? expect.stringMatching( - /file:.*\/.next\/server\/.*\/vercel\.[0-9a-f]{8}\.png$/ - ) - : expect.stringMatching( - /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ - ), + url: process.env.IS_TURBOPACK_TEST ? serverFilePath : clientFilePath, }) }) @@ -115,18 +110,10 @@ describe(`Handle new URL asset references`, () => { expect(data).toEqual({ imported: expect.objectContaining({ - src: expect.stringMatching( - /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ - ), + src: clientFilePath, }), // TODO Webpack bug? - url: process.env.IS_TURBOPACK_TEST - ? expect.stringMatching( - /file:.*\/.next\/server\/.*\/vercel\.[0-9a-f]{8}\.png$/ - ) - : expect.stringMatching( - /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ - ), + url: process.env.IS_TURBOPACK_TEST ? serverFilePath : clientFilePath, }) }) }) @@ -172,13 +159,9 @@ describe(`Handle new URL asset references`, () => { expect(data).toEqual({ imported: expect.objectContaining({ - src: expect.stringMatching( - /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ - ), + src: clientFilePath, }), - url: expect.stringMatching( - /file:.*\/.next\/server\/.*\/vercel\.[0-9a-f]{8}\.png$/ - ), + url: serverFilePath, size: 30079, }) }) @@ -190,11 +173,9 @@ describe(`Handle new URL asset references`, () => { expect(data).toEqual({ imported: expect.objectContaining({ - src: expect.stringMatching( - /^\/_next\/static\/media\/vercel\.[0-9a-f]{8}\.png$/ - ), + src: clientFilePath, }), - url: expect.stringMatching(/^blob:.*vercel\.[0-9a-f]{8,}\.png$/), + url: serverEdgeUrl, }) }) })