Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(turbopack): Apply tree shaking to all user codes #69344

Closed
wants to merge 17 commits into from
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>>(),
)
}
2 changes: 2 additions & 0 deletions test/development/basic/theme-ui/theme-ui.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { join } from 'path'
import { nextTestSetup } from 'e2e-utils'

jest.setTimeout(300000)

describe('theme-ui SWC option', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'fixture'),
Expand Down
2 changes: 2 additions & 0 deletions test/development/jsconfig-path-reloading/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { join } from 'path'
import webdriver from 'next-webdriver'
import fs from 'fs-extra'

jest.setTimeout(300000)

describe('jsconfig-path-reloading', () => {
let next: NextInstance
const tsConfigFile = 'jsconfig.json'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
import type { Response } from 'playwright'

jest.setTimeout(300000)

describe('app-dir action progressive enhancement', () => {
const { next } = nextTestSetup({
files: __dirname,
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app-edge-root-layout/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { nextTestSetup } from 'e2e-utils'

jest.setTimeout(450000)

describe('app-dir edge runtime root layout', () => {
const { next, isNextStart, skipped } = nextTestSetup({
files: __dirname,
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app-esm-js/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { nextTestSetup } from 'e2e-utils'

jest.setTimeout(180000)

describe('app-dir - esm js extension', () => {
const { next } = nextTestSetup({
files: __dirname,
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app-static/app-static.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import stripAnsi from 'strip-ansi'

const glob = promisify(globOrig)

jest.setTimeout(300000)

describe('app-dir static/dynamic handling', () => {
const { next, isNextDev, isNextStart, isNextDeploy } = nextTestSetup({
files: __dirname,
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app/experimental-compile.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'e2e-utils'

jest.setTimeout(300000)

process.env.NEXT_EXPERIMENTAL_COMPILE = '1'

if ((global as any).isNextStart) {
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { check, retry, waitFor } from 'next-test-utils'
import cheerio from 'cheerio'
import stripAnsi from 'strip-ansi'

jest.setTimeout(300000)

// TODO: We should decide on an established pattern for gating test assertions
// on experimental flags. For example, as a first step we could all the common
// gates like this one into a single module.
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app/useReportWebVitals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { createNext } from 'e2e-utils'
import { NextInstance } from 'e2e-utils'
import { check } from 'next-test-utils'

jest.setTimeout(180000)

describe('useReportWebVitals hook', () => {
let next: NextInstance

Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/logging/fetch-logging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import stripAnsi from 'strip-ansi'
import { retry } from 'next-test-utils'
import { nextTestSetup } from 'e2e-utils'

jest.setTimeout(300000)

const cacheReasonRegex = /Cache (missed|skipped) reason: /

interface ParsedLog {
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/logging/fetch-warning.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { retry } from 'next-test-utils'
import { nextTestSetup } from 'e2e-utils'

jest.setTimeout(180000)

describe('app-dir - fetch warnings', () => {
const { next, skipped, isNextDev } = nextTestSetup({
skipDeployment: true,
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/metadata-dynamic-routes/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { nextTestSetup } from 'e2e-utils'
import imageSize from 'image-size'
import { check } from 'next-test-utils'

jest.setTimeout(180000)

const CACHE_HEADERS = {
NONE: 'no-cache, no-store',
LONG: 'public, immutable, no-transform, max-age=31536000',
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/metadata-edge/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { nextTestSetup } from 'e2e-utils'
import imageSize from 'image-size'

jest.setTimeout(180000)

describe('app dir - Metadata API on the Edge runtime', () => {
const { next, isNextStart } = nextTestSetup({
files: __dirname,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'

jest.setTimeout(300000)

describe('parallel-routes-catchall', () => {
const { next } = nextTestSetup({
files: __dirname,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { nextTestSetup } from 'e2e-utils'
import { check, retry } from 'next-test-utils'

jest.setTimeout(300000)

describe('parallel-routes-revalidation', () => {
const { next, isNextStart, isNextDeploy } = nextTestSetup({
files: __dirname,
Expand Down
Loading
Loading