Skip to content

Commit

Permalink
feat(turbopack): Enable tree shaking (#70114)
Browse files Browse the repository at this point in the history
### What?

 - Supersedes #69344.
 - Enable tree shaking for production builds


### Why?

The history of #69344 is too messy

### How?

Closes PACK-3215
  • Loading branch information
kdy1 committed Sep 19, 2024
1 parent 48e3c14 commit 5307de9
Show file tree
Hide file tree
Showing 15 changed files with 344 additions and 52 deletions.
120 changes: 108 additions & 12 deletions crates/next-api/src/server_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ use next_core::{
next_manifests::{ActionLayer, ActionManifestWorkerEntry, ServerReferenceManifest},
util::NextRuntime,
};
use swc_core::{common::comments::Comments, ecma::ast::Program};
use swc_core::{
atoms::Atom,
common::comments::Comments,
ecma::{
ast::{Decl, ExportSpecifier, Id, ModuleDecl, ModuleItem, Program},
utils::find_pat_ids,
},
};
use tracing::Instrument;
use turbo_tasks::{
graph::{GraphTraversal, NonDeterministic},
Expand All @@ -22,11 +29,13 @@ use turbopack_core::{
output::OutputAsset,
reference::primary_referenced_modules,
reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType},
resolve::ModulePart,
virtual_output::VirtualOutputAsset,
virtual_source::VirtualSource,
};
use turbopack_ecmascript::{
chunk::EcmascriptChunkPlaceable, parse::ParseResult, EcmascriptParsable,
chunk::EcmascriptChunkPlaceable, parse::ParseResult,
tree_shake::asset::EcmascriptModulePartAsset, EcmascriptParsable,
};

/// Scans the RSC entry point's full module graph looking for exported Server
Expand Down Expand Up @@ -249,9 +258,9 @@ async fn get_referenced_modules(
///
/// Action names are stored in a leading BlockComment prefixed by
/// `__next_internal_action_entry_do_not_use__`.
pub fn parse_server_actions<C: Comments>(
pub fn parse_server_actions(
program: &Program,
comments: C,
comments: &dyn Comments,
) -> Option<BTreeMap<String, String>> {
let byte_pos = match program {
Program::Module(m) => m.span.lo,
Expand All @@ -268,37 +277,124 @@ pub fn parse_server_actions<C: Comments>(
})
})
}

/// Inspects the comments inside [Module] looking for the magic actions comment.
/// If found, we return the mapping of every action's hashed id to the name of
/// the exported action function. If not, we return a None.
#[turbo_tasks::function]
async fn parse_actions(module: Vc<Box<dyn Module>>) -> Result<Vc<OptionActionMap>> {
let parsed = if let Some(ecmascript_asset) =
let Some(ecmascript_asset) =
Vc::try_resolve_sidecast::<Box<dyn EcmascriptParsable>>(module).await?
{
ecmascript_asset.parse_original()
} else {
else {
return Ok(OptionActionMap::none());
};

if let Some(module) = Vc::try_resolve_downcast_type::<EcmascriptModulePartAsset>(module).await?
{
if matches!(
&*module.await?.part.await?,
ModulePart::Evaluation
| ModulePart::Exports
| ModulePart::Facade
| ModulePart::Internal(..)
) {
return Ok(OptionActionMap::none());
}
}

let original_parsed = ecmascript_asset.parse_original().resolve().await?;

let ParseResult::Ok {
comments, program, ..
} = &*parsed.await?
program: original,
comments,
..
} = &*original_parsed.await?
else {
// The file might be parse-able, but this is reported separately.
return Ok(OptionActionMap::none());
};

let Some(actions) = parse_server_actions(program, comments.clone()) else {
let Some(mut actions) = parse_server_actions(original, comments) else {
return Ok(OptionActionMap::none());
};

let fragment = ecmascript_asset.failsafe_parse().resolve().await?;

if fragment != original_parsed {
let ParseResult::Ok {
program: fragment, ..
} = &*fragment.await?
else {
// The file might be be parse-able, but this is reported separately.
return Ok(OptionActionMap::none());
};

let all_exports = all_export_names(fragment);
actions.retain(|_, name| all_exports.iter().any(|export| export == name));
}

let mut actions = IndexMap::from_iter(actions.into_iter());
actions.sort_keys();
Ok(Vc::cell(Some(Vc::cell(actions))))
}

fn all_export_names(program: &Program) -> Vec<Atom> {
match program {
Program::Module(m) => {
let mut exports = Vec::new();
for item in m.body.iter() {
match item {
ModuleItem::ModuleDecl(
ModuleDecl::ExportDefaultExpr(..) | ModuleDecl::ExportDefaultDecl(..),
) => {
exports.push("default".into());
}
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(decl)) => match &decl.decl {
Decl::Class(c) => {
exports.push(c.ident.sym.clone());
}
Decl::Fn(f) => {
exports.push(f.ident.sym.clone());
}
Decl::Var(v) => {
let ids: Vec<Id> = find_pat_ids(v);
exports.extend(ids.into_iter().map(|id| id.0));
}
_ => {}
},
ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(decl)) => {
for s in decl.specifiers.iter() {
match s {
ExportSpecifier::Named(named) => {
exports.push(
named
.exported
.as_ref()
.unwrap_or(&named.orig)
.atom()
.clone(),
);
}
ExportSpecifier::Default(_) => {
exports.push("default".into());
}
ExportSpecifier::Namespace(e) => {
exports.push(e.name.atom().clone());
}
}
}
}
_ => {}
}
}
exports
}

_ => {
vec![]
}
}
}

/// Converts our cached [parse_actions] call into a data type suitable for
/// collecting into a flat-mapped [IndexMap].
async fn parse_actions_filter_map(
Expand Down
2 changes: 2 additions & 0 deletions crates/next-core/src/next_client/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use crate::{
get_next_client_resolved_map,
},
next_shared::{
next_js_special_exports,
resolve::{
get_invalid_server_only_resolve_plugin, ModuleFeatureReportResolvePlugin,
NextSharedRuntimeResolvePlugin,
Expand Down Expand Up @@ -289,6 +290,7 @@ pub async fn get_client_module_options_context(
tree_shaking_mode: tree_shaking_mode_for_user_code,
enable_postcss_transform,
side_effect_free_packages: next_config.optimize_package_imports().await?.clone_value(),
special_exports: Some(next_js_special_exports()),
..Default::default()
};

Expand Down
8 changes: 6 additions & 2 deletions crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1149,9 +1149,13 @@ impl NextConfig {
#[turbo_tasks::function]
pub async fn tree_shaking_mode_for_user_code(
self: Vc<Self>,
_is_development: bool,
is_development: bool,
) -> Result<Vc<OptionTreeShaking>> {
Ok(Vc::cell(Some(TreeShakingMode::ReexportsOnly)))
Ok(Vc::cell(Some(if is_development {
TreeShakingMode::ReexportsOnly
} else {
TreeShakingMode::ModuleFragments
})))
}

#[turbo_tasks::function]
Expand Down
2 changes: 1 addition & 1 deletion crates/next-core/src/next_manifests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ pub enum ActionManifestWorkerEntry<'a> {
Serialize,
Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "kebab-case")]
pub enum ActionLayer {
Rsc,
ActionBrowser,
Expand Down
2 changes: 2 additions & 0 deletions crates/next-core/src/next_server/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ use crate::{
next_import_map::get_next_server_import_map,
next_server::resolve::ExternalPredicate,
next_shared::{
next_js_special_exports,
resolve::{
get_invalid_client_only_resolve_plugin, get_invalid_styled_jsx_resolve_plugin,
ModuleFeatureReportResolvePlugin, NextExternalResolvePlugin,
Expand Down Expand Up @@ -500,6 +501,7 @@ pub async fn get_server_module_options_context(
},
tree_shaking_mode: tree_shaking_mode_for_user_code,
side_effect_free_packages: next_config.optimize_package_imports().await?.clone_value(),
special_exports: Some(next_js_special_exports()),
..Default::default()
};

Expand Down
28 changes: 28 additions & 0 deletions crates/next-core/src/next_shared/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
use turbo_tasks::{RcStr, Vc};

pub(crate) mod resolve;
pub(crate) mod transforms;
pub(crate) mod webpack_rules;

#[turbo_tasks::function]
pub fn next_js_special_exports() -> Vc<Vec<RcStr>> {
Vc::cell(
[
"config",
"middleware",
"runtime",
"revalidate",
"dynamic",
"dynamicParams",
"fetchCache",
"preferredRegion",
"maxDuration",
"generateStaticParams",
"metadata",
"generateMetadata",
"getServerSideProps",
"getInitialProps",
"getStaticProps",
]
.into_iter()
.map(RcStr::from)
.collect::<Vec<RcStr>>(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ describe('actions-tree-shaking - reexport', () => {

expect(actionsRoutesState).toMatchObject({
'app/namespace-reexport/server/page': {
rsc: 1,
// Turbopack does not tree-shake server side chunks
rsc: process.env.TURBOPACK ? 3 : 1,
},
'app/namespace-reexport/client/page': {
'action-browser': 1,
// Turbopack does not support tree-shaking export * as we don't have global information
'action-browser': process.env.TURBOPACK ? 3 : 1,
},
// We're not able to tree-shake these re-exports here
// We're not able to tree-shake these re-exports here in webpack mode
'app/named-reexport/server/page': {
rsc: 3,
// Turbopack supports tree-shaking these re-exports
rsc: process.env.TURBOPACK ? 1 : 3,
},
'app/named-reexport/client/page': {
'action-browser': 3,
Expand Down
36 changes: 18 additions & 18 deletions test/turbopack-build-tests-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9056,13 +9056,13 @@
"Edge runtime code with imports Middleware importing unused 3rd party module production mode does not build and reports module not found error",
"Edge runtime code with imports Middleware importing unused 3rd party module throws not-found module error in dev at runtime and highlights the faulty line",
"Edge runtime code with imports Middleware importing unused node.js module does not throw in dev at runtime",
"Edge runtime code with imports Middleware statically importing node.js module throws unsupported module error in dev at runtime and highlights the faulty line",
"Edge runtime code with imports Edge API statically importing node.js module production mode throws unsupported module error in production at runtime and prints error on logs",
"Edge runtime code with imports Middleware statically importing node.js module production mode throws unsupported module error in production at runtime and prints error on logs"
"Edge runtime code with imports Middleware statically importing node.js module throws unsupported module error in dev at runtime and highlights the faulty line"
],
"failed": [
"Edge runtime code with imports Edge API importing unused node.js module production mode does not throw in production at runtime",
"Edge runtime code with imports Middleware importing unused node.js module production mode does not throw in production at runtime"
"Edge runtime code with imports Middleware importing unused node.js module production mode does not throw in production at runtime",
"Edge runtime code with imports Edge API statically importing node.js module production mode throws unsupported module error in production at runtime and prints error on logs",
"Edge runtime code with imports Middleware statically importing node.js module production mode throws unsupported module error in production at runtime and prints error on logs"
],
"pending": [],
"flakey": [],
Expand Down Expand Up @@ -15608,64 +15608,64 @@
"runtimeError": false
},
"test/production/app-dir/actions-tree-shaking/basic/basic.test.ts": {
"passed": [],
"failed": [
"passed": [
"actions-tree-shaking - basic should not have the unused action in the manifest"
],
"failed": [],
"pending": [],
"flakey": [],
"runtimeError": false
},
"test/production/app-dir/actions-tree-shaking/mixed-module-actions/mixed-module-actions-edge.test.ts": {
"passed": [],
"failed": [
"passed": [
"actions-tree-shaking - mixed-module-actions should not do tree shake for cjs module when import server actions"
],
"failed": [],
"pending": [],
"flakey": [],
"runtimeError": false
},
"test/production/app-dir/actions-tree-shaking/mixed-module-actions/mixed-module-actions.test.ts": {
"passed": [],
"failed": [
"passed": [
"actions-tree-shaking - mixed-module-actions should not do tree shake for cjs module when import server actions"
],
"failed": [],
"pending": [],
"flakey": [],
"runtimeError": false
},
"test/production/app-dir/actions-tree-shaking/reexport/reexport-edge.test.ts": {
"passed": [],
"failed": [
"passed": [
"actions-tree-shaking - reexport should not have the unused action in the manifest"
],
"failed": [],
"pending": [],
"flakey": [],
"runtimeError": false
},
"test/production/app-dir/actions-tree-shaking/reexport/reexport.test.ts": {
"passed": [],
"failed": [
"passed": [
"actions-tree-shaking - reexport should not have the unused action in the manifest"
],
"failed": [],
"pending": [],
"flakey": [],
"runtimeError": false
},
"test/production/app-dir/actions-tree-shaking/shared-module-actions/shared-module-actions-edge.test.ts": {
"passed": [],
"failed": [
"passed": [
"actions-tree-shaking - shared-module-actions should not have the unused action in the manifest"
],
"failed": [],
"pending": [],
"flakey": [],
"runtimeError": false
},
"test/production/app-dir/actions-tree-shaking/shared-module-actions/shared-module-actions.test.ts": {
"passed": [],
"failed": [
"passed": [
"actions-tree-shaking - shared-module-actions should not have the unused action in the manifest"
],
"failed": [],
"pending": [],
"flakey": [],
"runtimeError": false
Expand Down
Loading

0 comments on commit 5307de9

Please sign in to comment.