diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index 1f142320e8e59..d2571c9a94af9 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 cd780efb06015..091d734f40cca 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 286748d7336b8..81c0ad1f2e310 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 0d25bd170ee44..8628016ff52f0 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/test/e2e/url/app/api/edge/route.js b/test/e2e/url/app/api/edge/route.js new file mode 100644 index 0000000000000..c105ed9d4e01d --- /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/api/route.js b/test/e2e/url/app/api/route.js new file mode 100644 index 0000000000000..8e7a6b6dfd49e --- /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-edge/page.js b/test/e2e/url/app/client-edge/page.js new file mode 100644 index 0000000000000..8b5aea8a1e967 --- /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/client/page.js b/test/e2e/url/app/client/page.js new file mode 100644 index 0000000000000..810acb958590d --- /dev/null +++ b/test/e2e/url/app/client/page.js @@ -0,0 +1,12 @@ +'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} +
+ ) +} diff --git a/test/e2e/url/app/layout.js b/test/e2e/url/app/layout.js new file mode 100644 index 0000000000000..4ee00a218505a --- /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 0000000000000..b19203108ff7d --- /dev/null +++ b/test/e2e/url/app/manifest.js @@ -0,0 +1,17 @@ +import icon from '../public/vercel.png' +const url = new URL('../public/vercel.png', import.meta.url).toString() + +export default function manifest() { + return { + short_name: 'Next.js', + name: 'Next.js', + icons: [ + { + src: icon.src, + type: 'image/png', + sizes: '512x512', + }, + ], + description: url, + } +} diff --git a/test/e2e/url/app/opengraph-image.js b/test/e2e/url/app/opengraph-image.js new file mode 100644 index 0000000000000..07cd809ad1b0f --- /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/app/rsc-edge/page.js b/test/e2e/url/app/rsc-edge/page.js new file mode 100644 index 0000000000000..b673e2eb62fdc --- /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/app/rsc/page.js b/test/e2e/url/app/rsc/page.js new file mode 100644 index 0000000000000..ce35c94a1ec36 --- /dev/null +++ b/test/e2e/url/app/rsc/page.js @@ -0,0 +1,10 @@ +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} +
+ ) +} diff --git a/test/e2e/url/middleware.ts b/test/e2e/url/middleware.ts new file mode 100644 index 0000000000000..1d87d3d5bb57c --- /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).toString() + +export async function middleware(req: NextRequest) { + if (req.nextUrl.toString().endsWith('/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/basename.js deleted file mode 100644 index 0109c99b7b881..0000000000000 --- a/test/e2e/url/pages/api/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-edge/index.js b/test/e2e/url/pages/api/pages-edge/index.js new file mode 100644 index 0000000000000..d124c191ac55e --- /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 new file mode 100644 index 0000000000000..4da25e8db01da --- /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.send({ + imported, + url: url.toString(), + size: fs.readFileSync(url).length, + }) +} diff --git a/test/e2e/url/pages/api/size.js b/test/e2e/url/pages/api/size.js deleted file mode 100644 index 7649a0bcb1307..0000000000000 --- a/test/e2e/url/pages/api/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-edge/ssr.js b/test/e2e/url/pages/pages-edge/ssr.js new file mode 100644 index 0000000000000..de5792607d3e7 --- /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 0000000000000..de324501b4d9b --- /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/pages/pages/ssg.js b/test/e2e/url/pages/pages/ssg.js new file mode 100644 index 0000000000000..fe8d852cca225 --- /dev/null +++ b/test/e2e/url/pages/pages/ssg.js @@ -0,0 +1,18 @@ +import imported from '../../public/vercel.png' + +export async function getStaticProps() { + 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} +
+ ) +} diff --git a/test/e2e/url/pages/pages/ssr.js b/test/e2e/url/pages/pages/ssr.js new file mode 100644 index 0000000000000..ca2572a4b8515 --- /dev/null +++ b/test/e2e/url/pages/pages/ssr.js @@ -0,0 +1,18 @@ +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} +
+ ) +} diff --git a/test/e2e/url/pages/pages/static.js b/test/e2e/url/pages/pages/static.js new file mode 100644 index 0000000000000..ce35c94a1ec36 --- /dev/null +++ b/test/e2e/url/pages/pages/static.js @@ -0,0 +1,10 @@ +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} +
+ ) +} diff --git a/test/e2e/url/pages/ssg.js b/test/e2e/url/pages/ssg.js deleted file mode 100644 index 113f47c56552f..0000000000000 --- 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 6aec1e94a0f72..0000000000000 --- 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 03bc185ce9f08..0000000000000 --- 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 7a11eff4aaa65..3b645c2e0eccd 100644 --- a/test/e2e/url/url.test.ts +++ b/test/e2e/url/url.test.ts @@ -1,46 +1,182 @@ -import { getBrowserBodyText, retry } from 'next-test-utils' +import { retry } from 'next-test-utils' import { nextTestSetup } from 'e2e-utils' +// | | 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 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 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, '') + 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') + .then((res) => res.ok && res.json()) - for (const page of ['/static', '/ssr', '/ssg']) { - it(`should render the ${page} page`, async () => { - const html = await next.render(page) - expect(html).toMatch(expectedServer) + expect(data).toEqual({ + imported: expect.objectContaining({ + src: clientFilePath, + }), + url: serverEdgeUrl, }) + }) - it(`should client-render the ${page} page`, async () => { - const browser = await next.browser(page) - await retry(async () => - expect(await getBrowserBodyText(browser)).toMatch(expectedClient) - ) + const expectedPage = + /^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: clientFilePath, + type: 'image/png', + sizes: '512x512', + }, + ], + // TODO Webpack bug? + description: process.env.IS_TURBOPACK_TEST + ? serverFilePath + : clientFilePath, + }) }) - } - it('should respond on size api', async () => { - const data = await next - .fetch('/api/size') - .then((res) => res.ok && res.json()) + 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: clientFilePath, + }), + // TODO Webpack bug? + url: process.env.IS_TURBOPACK_TEST ? serverFilePath : clientFilePath, + }) + }) + + for (const page of ['/rsc', '/rsc-edge', '/client', '/client-edge']) { + // TODO Webpack bug? + let shouldSkip = process.env.IS_TURBOPACK_TEST + ? false + : page.includes('edge') - expect(data).toEqual({ size: 30079 }) + ;(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 () => { + const data = await next.fetch('/api').then((res) => res.ok && res.json()) + + expect(data).toEqual({ + imported: expect.objectContaining({ + src: clientFilePath, + }), + // TODO Webpack bug? + url: process.env.IS_TURBOPACK_TEST ? serverFilePath : clientFilePath, + }) + }) }) - it('should respond on basename api', async () => { - const data = await next - .fetch('/api/basename') - .then((res) => res.ok && res.json()) + describe('pages router', () => { + for (const page of [ + '/pages/static', + '/pages/ssr', + '/pages/ssg', + '/pages-edge/static', + '/pages-edge/ssr', + ]) { + // TODO Webpack bug? + let shouldSkip = process.env.IS_TURBOPACK_TEST + ? false + : page.includes('edge') - expect(data).toEqual({ - basename: expect.stringMatching(/^vercel\.[0-9a-f]{8}\.png$/), + ;(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 () => { + const data = await next + .fetch('/api/pages/') + .then((res) => res.ok && res.json()) + + expect(data).toEqual({ + imported: expect.objectContaining({ + src: clientFilePath, + }), + url: serverFilePath, + 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: clientFilePath, + }), + url: serverEdgeUrl, + }) }) }) }) diff --git a/turbopack/crates/turbopack-browser/src/chunking_context.rs b/turbopack/crates/turbopack-browser/src/chunking_context.rs index f8903e058525b..dcdf2158154b7 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,27 @@ 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 = tag + .as_ref() + .and_then(|tag| self.client_roots.get(tag)) + .unwrap_or(&self.client_root); + + 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!("{}/", 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 +570,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 +585,13 @@ impl ChunkingContext for BrowserChunkingContext { content_hash = &content_hash[..8] ), }; - Ok(self.asset_root_path.join(&asset_path)?.cell()) + + 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()) } #[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 519b1cb1bb66f..2d7b3d5676183 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 e19d1dcb7f800..c877f39598bee 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,27 @@ 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 = tag + .as_ref() + .and_then(|tag| self.client_roots.get(tag)) + .unwrap_or(&self.client_root); + + 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!("{}/", 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 +401,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 +416,13 @@ impl ChunkingContext for NodeJsChunkingContext { content_hash = &content_hash[..8] ), }; - Ok(self.asset_root_path.join(&asset_path)?.cell()) + + 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()) } #[turbo_tasks::function] diff --git a/turbopack/crates/turbopack-static/src/css.rs b/turbopack/crates/turbopack-static/src/css.rs index 04f2724edcc9a..b689eedada1b6 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 e901981595e67..cf5530fdfacb8 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 c1c4b868c2604..3d9600f5e078a 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 24fc0d504df7b..565fd0381c977 100644 --- a/turbopack/crates/turbopack/src/lib.rs +++ b/turbopack/crates/turbopack/src/lib.rs @@ -271,12 +271,16 @@ async fn apply_module_type( .to_resolved() .await?, ), - ModuleType::StaticUrlJs => { - ResolvedVc::upcast(StaticUrlJsModule::new(*source).to_resolved().await?) - } - ModuleType::StaticUrlCss => { - ResolvedVc::upcast(StaticUrlCssModule::new(*source).to_resolved().await?) - } + ModuleType::StaticUrlJs { tag } => ResolvedVc::upcast( + StaticUrlJsModule::new(*source, tag.clone()) + .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 2de0143c34ff5..327d87be986bf 100644 --- a/turbopack/crates/turbopack/src/module_options/mod.rs +++ b/turbopack/crates/turbopack/src/module_options/mod.rs @@ -434,15 +434,29 @@ 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)), - 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 f1e017c1d3ee1..319a686625914 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,8 +139,12 @@ pub enum ModuleType { ty: CssModuleAssetType, environment: Option>, }, - StaticUrlJs, - StaticUrlCss, + StaticUrlJs { + tag: Option, + }, + StaticUrlCss { + tag: Option, + }, InlinedBytesJs, WebAssembly { source_ty: WebAssemblySourceType,