feat(turbopack): add support for esm externals in app dir (#64918)
### What? Writes the async flag to the client reference manifest to support ESM externals in app dir. The `app-ssr` context can't have esm externals as that might cause a module to be async in ssr but not on the client. Closes PACK-2967 Fixes #64525
This commit is contained in:
parent
42f6dc3d9c
commit
d02dfc6e05
6 changed files with 180 additions and 121 deletions
|
@ -848,13 +848,13 @@ impl AppEndpoint {
|
|||
{
|
||||
entry_client_chunks.extend(chunks.await?.iter().copied());
|
||||
}
|
||||
for chunks in client_references_chunks_ref
|
||||
for (chunks, _) in client_references_chunks_ref
|
||||
.client_component_client_chunks
|
||||
.values()
|
||||
{
|
||||
client_assets.extend(chunks.await?.iter().copied());
|
||||
}
|
||||
for chunks in client_references_chunks_ref
|
||||
for (chunks, _) in client_references_chunks_ref
|
||||
.client_component_ssr_chunks
|
||||
.values()
|
||||
{
|
||||
|
@ -948,7 +948,7 @@ impl AppEndpoint {
|
|||
// initialization
|
||||
let client_references_chunks = &*client_references_chunks.await?;
|
||||
|
||||
for &ssr_chunks in client_references_chunks
|
||||
for (ssr_chunks, _) in client_references_chunks
|
||||
.client_component_ssr_chunks
|
||||
.values()
|
||||
{
|
||||
|
|
|
@ -29,8 +29,10 @@ fn client_modules_ssr_modifier() -> Vc<RcStr> {
|
|||
|
||||
#[turbo_tasks::value]
|
||||
pub struct ClientReferencesChunks {
|
||||
pub client_component_client_chunks: IndexMap<ClientReferenceType, Vc<OutputAssets>>,
|
||||
pub client_component_ssr_chunks: IndexMap<ClientReferenceType, Vc<OutputAssets>>,
|
||||
pub client_component_client_chunks:
|
||||
IndexMap<ClientReferenceType, (Vc<OutputAssets>, AvailabilityInfo)>,
|
||||
pub client_component_ssr_chunks:
|
||||
IndexMap<ClientReferenceType, (Vc<OutputAssets>, AvailabilityInfo)>,
|
||||
pub layout_segment_client_chunks: IndexMap<Vc<NextServerComponentModule>, Vc<OutputAssets>>,
|
||||
}
|
||||
|
||||
|
@ -63,23 +65,47 @@ pub async fn get_app_client_references_chunks(
|
|||
) => {
|
||||
let ecmascript_client_reference_ref =
|
||||
ecmascript_client_reference.await?;
|
||||
(
|
||||
client_chunking_context.root_chunk_group_assets(Vc::upcast(
|
||||
|
||||
let client_chunk_group = client_chunking_context
|
||||
.root_chunk_group(Vc::upcast(
|
||||
ecmascript_client_reference_ref.client_module,
|
||||
)),
|
||||
ssr_chunking_context.map(|ssr_chunking_context| {
|
||||
ssr_chunking_context.root_chunk_group_assets(Vc::upcast(
|
||||
))
|
||||
.await?;
|
||||
|
||||
(
|
||||
(
|
||||
client_chunk_group.assets,
|
||||
client_chunk_group.availability_info,
|
||||
),
|
||||
if let Some(ssr_chunking_context) = ssr_chunking_context {
|
||||
let ssr_chunk_group = ssr_chunking_context
|
||||
.root_chunk_group(Vc::upcast(
|
||||
ecmascript_client_reference_ref.ssr_module,
|
||||
))
|
||||
}),
|
||||
.await?;
|
||||
|
||||
Some((
|
||||
ssr_chunk_group.assets,
|
||||
ssr_chunk_group.availability_info,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
}
|
||||
ClientReferenceType::CssClientReference(css_client_reference) => {
|
||||
let css_client_reference_ref = css_client_reference.await?;
|
||||
(
|
||||
client_chunking_context.root_chunk_group_assets(Vc::upcast(
|
||||
let client_chunk_group = client_chunking_context
|
||||
.root_chunk_group(Vc::upcast(
|
||||
css_client_reference_ref.client_module,
|
||||
)),
|
||||
))
|
||||
.await?;
|
||||
|
||||
(
|
||||
(
|
||||
client_chunk_group.assets,
|
||||
client_chunk_group.availability_info,
|
||||
),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
@ -162,6 +188,7 @@ pub async fn get_app_client_references_chunks(
|
|||
})
|
||||
.try_flat_join()
|
||||
.await?;
|
||||
|
||||
let ssr_chunk_group = if !ssr_modules.is_empty() {
|
||||
ssr_chunking_context.map(|ssr_chunking_context| {
|
||||
let _span = tracing::info_span!(
|
||||
|
@ -182,6 +209,7 @@ pub async fn get_app_client_references_chunks(
|
|||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let client_modules = client_reference_types
|
||||
.iter()
|
||||
.map(|client_reference_ty| async move {
|
||||
|
@ -238,8 +266,10 @@ pub async fn get_app_client_references_chunks(
|
|||
if let ClientReferenceType::EcmascriptClientReference(_) =
|
||||
client_reference_ty
|
||||
{
|
||||
client_component_client_chunks
|
||||
.insert(client_reference_ty, client_chunks);
|
||||
client_component_client_chunks.insert(
|
||||
client_reference_ty,
|
||||
(client_chunks, client_chunk_group.availability_info),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -259,7 +289,10 @@ pub async fn get_app_client_references_chunks(
|
|||
if let ClientReferenceType::EcmascriptClientReference(_) =
|
||||
client_reference_ty
|
||||
{
|
||||
client_component_ssr_chunks.insert(client_reference_ty, ssr_chunks);
|
||||
client_component_ssr_chunks.insert(
|
||||
client_reference_ty,
|
||||
(ssr_chunks, ssr_chunk_group.availability_info),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,10 @@ use turbo_tasks_fs::{File, FileSystemPath};
|
|||
use turbopack_binding::turbopack::{
|
||||
core::{
|
||||
asset::AssetContent,
|
||||
chunk::{ChunkItemExt, ChunkableModule, ChunkingContext, ModuleId as TurbopackModuleId},
|
||||
chunk::{
|
||||
availability_info::AvailabilityInfo, ChunkItem, ChunkItemExt, ChunkableModule,
|
||||
ChunkingContext, ModuleId as TurbopackModuleId,
|
||||
},
|
||||
output::OutputAsset,
|
||||
virtual_output::VirtualOutputAsset,
|
||||
},
|
||||
|
@ -66,13 +69,15 @@ impl ClientReferenceManifest {
|
|||
.to_string()
|
||||
.await?;
|
||||
|
||||
let client_module_id = ecmascript_client_reference
|
||||
let client_chunk_item = ecmascript_client_reference
|
||||
.client_module
|
||||
.as_chunk_item(Vc::upcast(client_chunking_context))
|
||||
.id()
|
||||
.await?;
|
||||
.as_chunk_item(Vc::upcast(client_chunking_context));
|
||||
|
||||
let client_chunks_paths = if let Some(client_chunks) = client_references_chunks
|
||||
let client_module_id = client_chunk_item.id().await?;
|
||||
|
||||
let (client_chunks_paths, client_is_async) =
|
||||
if let Some((client_chunks, client_availability_info)) =
|
||||
client_references_chunks
|
||||
.client_component_client_chunks
|
||||
.get(&app_client_reference_ty)
|
||||
{
|
||||
|
@ -83,7 +88,7 @@ impl ClientReferenceManifest {
|
|||
.try_join()
|
||||
.await?;
|
||||
|
||||
client_chunks_paths
|
||||
let chunk_paths = client_chunks_paths
|
||||
.iter()
|
||||
.filter_map(|chunk_path| client_relative_path.get_path_to(chunk_path))
|
||||
.map(ToString::to_string)
|
||||
|
@ -91,35 +96,41 @@ impl ClientReferenceManifest {
|
|||
// be handled separatedly.
|
||||
.filter(|path| path.ends_with(".js"))
|
||||
.map(RcStr::from)
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let is_async =
|
||||
is_item_async(client_availability_info, client_chunk_item).await?;
|
||||
|
||||
(chunk_paths, is_async)
|
||||
} else {
|
||||
Vec::new()
|
||||
(Vec::new(), false)
|
||||
};
|
||||
|
||||
entry_manifest.client_modules.module_exports.insert(
|
||||
get_client_reference_module_key(&server_path, "*"),
|
||||
ManifestNodeEntry {
|
||||
name: "*".into(),
|
||||
id: (&*client_module_id).into(),
|
||||
chunks: client_chunks_paths,
|
||||
// TODO(WEB-434)
|
||||
r#async: false,
|
||||
r#async: client_is_async,
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(ssr_chunking_context) = ssr_chunking_context {
|
||||
let ssr_module_id = ecmascript_client_reference
|
||||
let ssr_chunk_item = ecmascript_client_reference
|
||||
.ssr_module
|
||||
.as_chunk_item(Vc::upcast(ssr_chunking_context))
|
||||
.id()
|
||||
.await?;
|
||||
.as_chunk_item(Vc::upcast(ssr_chunking_context));
|
||||
|
||||
let ssr_chunks_paths = if runtime == NextRuntime::Edge {
|
||||
let ssr_module_id = ssr_chunk_item.id().await?;
|
||||
|
||||
let (ssr_chunks_paths, ssr_is_async) = if runtime == NextRuntime::Edge {
|
||||
// the chunks get added to the middleware-manifest.json instead
|
||||
// of this file because the
|
||||
// edge runtime doesn't support dynamically
|
||||
// loading chunks.
|
||||
Vec::new()
|
||||
} else if let Some(ssr_chunks) = client_references_chunks
|
||||
(Vec::new(), false)
|
||||
} else if let Some((ssr_chunks, ssr_availability_info)) =
|
||||
client_references_chunks
|
||||
.client_component_ssr_chunks
|
||||
.get(&app_client_reference_ty)
|
||||
{
|
||||
|
@ -131,15 +142,20 @@ impl ClientReferenceManifest {
|
|||
.try_join()
|
||||
.await?;
|
||||
|
||||
ssr_chunks_paths
|
||||
let chunk_paths = ssr_chunks_paths
|
||||
.iter()
|
||||
.filter_map(|chunk_path| node_root_ref.get_path_to(chunk_path))
|
||||
.map(ToString::to_string)
|
||||
.map(RcStr::from)
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let is_async = is_item_async(ssr_availability_info, ssr_chunk_item).await?;
|
||||
|
||||
(chunk_paths, is_async)
|
||||
} else {
|
||||
Vec::new()
|
||||
(Vec::new(), false)
|
||||
};
|
||||
|
||||
let mut ssr_manifest_node = ManifestNode::default();
|
||||
ssr_manifest_node.module_exports.insert(
|
||||
"*".into(),
|
||||
|
@ -147,8 +163,7 @@ impl ClientReferenceManifest {
|
|||
name: "*".into(),
|
||||
id: (&*ssr_module_id).into(),
|
||||
chunks: ssr_chunks_paths,
|
||||
// TODO(WEB-434)
|
||||
r#async: false,
|
||||
r#async: ssr_is_async,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -253,3 +268,18 @@ pub fn get_client_reference_module_key(server_path: &str, export_name: &str) ->
|
|||
format!("{}#{}", server_path, export_name).into()
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_item_async(
|
||||
availability_info: &AvailabilityInfo,
|
||||
chunk_item: Vc<Box<dyn ChunkItem>>,
|
||||
) -> Result<bool> {
|
||||
let Some(available_chunk_items) = availability_info.available_chunk_items() else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let Some(info) = &*available_chunk_items.get(chunk_item).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
Ok(info.is_async)
|
||||
}
|
||||
|
|
|
@ -174,14 +174,15 @@ pub async fn get_server_resolve_options_context(
|
|||
|
||||
external_packages.retain(|item| !transpile_packages.contains(item));
|
||||
|
||||
let ty = ty.into_value();
|
||||
|
||||
let server_external_packages_plugin = ExternalCjsModulesResolvePlugin::new(
|
||||
project_path,
|
||||
project_path.root(),
|
||||
ExternalPredicate::Only(Vc::cell(external_packages)).cell(),
|
||||
// TODO(sokra) esmExternals support
|
||||
false,
|
||||
// app-ssr can't have esm externals as that would make the module async on the server only
|
||||
*next_config.import_externals().await? && !matches!(ty, ServerContextType::AppSSR { .. }),
|
||||
);
|
||||
let ty = ty.into_value();
|
||||
|
||||
let mut custom_conditions = vec![mode.await?.condition().to_string().into()];
|
||||
custom_conditions.extend(
|
||||
|
|
|
@ -16,7 +16,7 @@ async function resolveStreamResponse(response: any, onData?: any) {
|
|||
}
|
||||
|
||||
describe('app dir - external dependency', () => {
|
||||
const { next, skipped, isTurbopack } = nextTestSetup({
|
||||
const { next, skipped } = nextTestSetup({
|
||||
files: __dirname,
|
||||
dependencies: {
|
||||
swr: 'latest',
|
||||
|
@ -286,10 +286,7 @@ describe('app dir - external dependency', () => {
|
|||
browser.elementByCss('#dual-pkg-outout button').click()
|
||||
await check(async () => {
|
||||
const text = await browser.elementByCss('#dual-pkg-outout p').text()
|
||||
// TODO: enable esm externals for app router in turbopack for actions
|
||||
expect(text).toBe(
|
||||
isTurbopack ? 'dual-pkg-optout:cjs' : 'dual-pkg-optout:mjs'
|
||||
)
|
||||
expect(text).toBe('dual-pkg-optout:mjs')
|
||||
return 'success'
|
||||
}, /success/)
|
||||
})
|
||||
|
|
|
@ -8,65 +8,63 @@ describe('esm-externals', () => {
|
|||
const { next, isTurbopack } = nextTestSetup({
|
||||
files: __dirname,
|
||||
})
|
||||
// Pages
|
||||
{
|
||||
const urls = ['/static', '/ssr', '/ssg']
|
||||
|
||||
for (const url of urls) {
|
||||
// Pages
|
||||
describe.each(['/static', '/ssr', '/ssg'])('pages url %s', (url) => {
|
||||
// For invalid esm packages that have "import" pointing to a non-esm-flagged module
|
||||
// webpack is using the CJS version instead but Turbopack is opting out of
|
||||
// webpack is using the CJS version instead, but Turbopack is opting out of
|
||||
// externalizing and bundling the non-esm-flagged module.
|
||||
const expectedHtml = isTurbopack
|
||||
? /Hello World\+World\+World\+World\+World\+World/
|
||||
? 'Hello World+World+World+World+World+World'
|
||||
: url === '/static'
|
||||
? /Hello World\+World\+Alternative\+World\+World\+World/
|
||||
: /Hello World\+World\+Alternative\+World\+World\+Alternative/
|
||||
// On client side, webpack always bundlings so it uses the non-esm-flagged module too.
|
||||
? 'Hello World+World+Alternative+World+World+World'
|
||||
: 'Hello World+World+Alternative+World+World+Alternative'
|
||||
|
||||
// On the client side, webpack always bundles, so it uses the non-esm-flagged module too.
|
||||
const expectedText =
|
||||
isTurbopack || url === '/static'
|
||||
? /Hello World\+World\+World\+World\+World\+World/
|
||||
: /Hello World\+World\+World\+World\+World\+Alternative/
|
||||
? 'Hello World+World+World+World+World+World'
|
||||
: 'Hello World+World+World+World+World+Alternative'
|
||||
|
||||
it(`should return the correct SSR HTML for ${url}`, async () => {
|
||||
const res = await next.fetch(url)
|
||||
const html = await res.text()
|
||||
expect(normalize(html)).toMatch(expectedHtml)
|
||||
it('should return the correct SSR HTML', async () => {
|
||||
const $ = await next.render$(url)
|
||||
const body = $('body > div > div').html()
|
||||
expect(normalize(body)).toEqual(expectedHtml)
|
||||
})
|
||||
|
||||
it(`should render the correct page for ${url}`, async () => {
|
||||
it('should render the correct page', async () => {
|
||||
const browser = await next.browser(url)
|
||||
expect(await browser.elementByCss('body').text()).toMatch(expectedText)
|
||||
expect(await browser.elementByCss('body > div').text()).toEqual(
|
||||
expectedText
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// App dir
|
||||
{
|
||||
// TODO App Dir doesn't use esmExternals: true correctly for Turbopack
|
||||
// so we only verify that the page doesn't crash, but ignore the actual content
|
||||
// const expectedHtml = isTurbopack ? /Hello World\+World\+World/ : /Hello World\+World\+Alternative/
|
||||
describe.each(['/server', '/client'])('app dir url %s', (url) => {
|
||||
const expectedHtml = isTurbopack
|
||||
? /Hello Wrong\+Wrong\+Alternative/
|
||||
: /Hello World\+World\+Alternative/
|
||||
const urls = ['/server', '/client']
|
||||
? url === '/client'
|
||||
? 'Hello Wrong+Wrong+Alternative'
|
||||
: 'Hello World+World+World'
|
||||
: 'Hello World+World+Alternative'
|
||||
|
||||
for (const url of urls) {
|
||||
const expectedText =
|
||||
url !== '/server'
|
||||
? /Hello World\+World\+World/
|
||||
: isTurbopack
|
||||
? /Hello Wrong\+Wrong\+Alternative/
|
||||
: /Hello World\+World\+Alternative/
|
||||
it(`should return the correct SSR HTML for ${url}`, async () => {
|
||||
const res = await next.fetch(url)
|
||||
const html = await res.text()
|
||||
expect(normalize(html)).toMatch(expectedHtml)
|
||||
const expectedText = isTurbopack
|
||||
? 'Hello World+World+World'
|
||||
: url === '/client'
|
||||
? 'Hello World+World+World'
|
||||
: 'Hello World+World+Alternative'
|
||||
|
||||
it('should return the correct SSR HTML', async () => {
|
||||
const $ = await next.render$(url)
|
||||
const body = $('body > div').html()
|
||||
expect(normalize(body)).toEqual(expectedHtml)
|
||||
})
|
||||
|
||||
it(`should render the correct page for ${url}`, async () => {
|
||||
it('should render the correct page', async () => {
|
||||
const browser = await next.browser(url)
|
||||
expect(await browser.elementByCss('body').text()).toMatch(expectedText)
|
||||
expect(await browser.elementByCss('body > div').text()).toEqual(
|
||||
expectedText
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue