next_core/next_client/
context.rs

1use std::collections::BTreeSet;
2
3use anyhow::Result;
4use bincode::{Decode, Encode};
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{ResolvedVc, TaskInput, Vc, trace::TraceRawVcs};
7use turbo_tasks_fs::FileSystemPath;
8use turbopack::module_options::{
9    CssOptionsContext, EcmascriptOptionsContext, JsxTransformOptions, ModuleRule,
10    TypescriptTransformOptions, module_options_context::ModuleOptionsContext,
11    side_effect_free_packages_glob,
12};
13use turbopack_browser::{
14    BrowserChunkingContext, ChunkSuffix, ContentHashing, CurrentChunkMethod,
15    react_refresh::assert_can_resolve_react_refresh,
16};
17use turbopack_core::{
18    chunk::{
19        ChunkingConfig, ChunkingContext, MangleType, MinifyType, SourceMapSourceType,
20        SourceMapsType, module_id_strategies::ModuleIdStrategy,
21    },
22    compile_time_info::{CompileTimeDefines, CompileTimeInfo, FreeVarReference, FreeVarReferences},
23    environment::{BrowserEnvironment, Environment, ExecutionEnvironment},
24    free_var_references,
25    module_graph::binding_usage_info::OptionBindingUsageInfo,
26    resolve::{parse::Request, pattern::Pattern},
27};
28use turbopack_css::chunk::CssChunkType;
29use turbopack_ecmascript::{AnalyzeMode, TypeofWindow, chunk::EcmascriptChunkType};
30use turbopack_node::{
31    execution_context::ExecutionContext,
32    transforms::postcss::{PostCssConfigLocation, PostCssTransformOptions},
33};
34use turbopack_resolve::resolve_options_context::ResolveOptionsContext;
35
36use crate::{
37    mode::NextMode,
38    next_build::get_postcss_package_mapping,
39    next_client::{
40        runtime_entry::{RuntimeEntries, RuntimeEntry},
41        transforms::get_next_client_transforms_rules,
42    },
43    next_config::NextConfig,
44    next_font::local::NextFontLocalResolvePlugin,
45    next_import_map::{
46        get_next_client_fallback_import_map, get_next_client_import_map,
47        get_next_client_resolved_map,
48    },
49    next_shared::{
50        resolve::{
51            ModuleFeatureReportResolvePlugin, NextSharedRuntimeResolvePlugin,
52            get_invalid_server_only_resolve_plugin,
53        },
54        transforms::{
55            emotion::get_emotion_transform_rule,
56            react_remove_properties::get_react_remove_properties_transform_rule,
57            relay::get_relay_transform_rule, remove_console::get_remove_console_transform_rule,
58            styled_components::get_styled_components_transform_rule,
59            styled_jsx::get_styled_jsx_transform_rule,
60            swc_ecma_transform_plugins::get_swc_ecma_transform_plugin_rule,
61        },
62        webpack_rules::{WebpackLoaderBuiltinCondition, webpack_loader_options},
63    },
64    transform_options::{
65        get_decorators_transform_options, get_jsx_transform_options,
66        get_typescript_transform_options,
67    },
68    util::{
69        OptionEnvMap, defines, foreign_code_context_condition, internal_assets_conditions,
70        module_styles_rule_condition,
71    },
72};
73
74#[turbo_tasks::function]
75async fn next_client_defines(define_env: Vc<OptionEnvMap>) -> Result<Vc<CompileTimeDefines>> {
76    Ok(defines(&*define_env.await?).cell())
77}
78
79#[turbo_tasks::function]
80async fn next_client_free_vars(define_env: Vc<OptionEnvMap>) -> Result<Vc<FreeVarReferences>> {
81    Ok(free_var_references!(
82        ..defines(&*define_env.await?).into_iter(),
83        Buffer = FreeVarReference::EcmaScriptModule {
84            request: rcstr!("node:buffer"),
85            lookup_path: None,
86            export: Some(rcstr!("Buffer")),
87        },
88        process = FreeVarReference::EcmaScriptModule {
89            request: rcstr!("node:process"),
90            lookup_path: None,
91            export: Some(rcstr!("default")),
92        }
93    )
94    .cell())
95}
96
97#[turbo_tasks::function]
98pub async fn get_client_compile_time_info(
99    browserslist_query: RcStr,
100    define_env: Vc<OptionEnvMap>,
101) -> Result<Vc<CompileTimeInfo>> {
102    CompileTimeInfo::builder(
103        Environment::new(ExecutionEnvironment::Browser(
104            BrowserEnvironment {
105                dom: true,
106                web_worker: false,
107                service_worker: false,
108                browserslist_query: browserslist_query.to_owned(),
109            }
110            .resolved_cell(),
111        ))
112        .to_resolved()
113        .await?,
114    )
115    .defines(next_client_defines(define_env).to_resolved().await?)
116    .free_var_references(next_client_free_vars(define_env).to_resolved().await?)
117    .cell()
118    .await
119}
120
121#[turbo_tasks::value(shared)]
122#[derive(Debug, Clone, Hash, TaskInput)]
123pub enum ClientContextType {
124    Pages { pages_dir: FileSystemPath },
125    App { app_dir: FileSystemPath },
126    Fallback,
127    Other,
128}
129
130#[turbo_tasks::function]
131pub async fn get_client_resolve_options_context(
132    project_path: FileSystemPath,
133    ty: ClientContextType,
134    mode: Vc<NextMode>,
135    next_config: Vc<NextConfig>,
136    execution_context: Vc<ExecutionContext>,
137) -> Result<Vc<ResolveOptionsContext>> {
138    let next_client_import_map = get_next_client_import_map(
139        project_path.clone(),
140        ty.clone(),
141        next_config,
142        mode,
143        execution_context,
144    )
145    .to_resolved()
146    .await?;
147    let next_client_fallback_import_map = get_next_client_fallback_import_map(ty.clone())
148        .to_resolved()
149        .await?;
150    let next_client_resolved_map =
151        get_next_client_resolved_map(project_path.clone(), project_path.clone(), *mode.await?)
152            .to_resolved()
153            .await?;
154    let mut custom_conditions: Vec<_> = mode.await?.custom_resolve_conditions().collect();
155
156    if *next_config.enable_cache_components().await? {
157        custom_conditions.push(rcstr!("next-js"));
158    };
159
160    let resolve_options_context = ResolveOptionsContext {
161        enable_node_modules: Some(project_path.root().owned().await?),
162        custom_conditions,
163        import_map: Some(next_client_import_map),
164        fallback_import_map: Some(next_client_fallback_import_map),
165        resolved_map: Some(next_client_resolved_map),
166        browser: true,
167        module: true,
168        before_resolve_plugins: vec![
169            ResolvedVc::upcast(
170                get_invalid_server_only_resolve_plugin(project_path.clone())
171                    .to_resolved()
172                    .await?,
173            ),
174            ResolvedVc::upcast(
175                ModuleFeatureReportResolvePlugin::new(project_path.clone())
176                    .to_resolved()
177                    .await?,
178            ),
179            ResolvedVc::upcast(
180                NextFontLocalResolvePlugin::new(project_path.clone())
181                    .to_resolved()
182                    .await?,
183            ),
184        ],
185        after_resolve_plugins: vec![ResolvedVc::upcast(
186            NextSharedRuntimeResolvePlugin::new(project_path.clone())
187                .to_resolved()
188                .await?,
189        )],
190        ..Default::default()
191    };
192
193    let tsconfig_path = next_config
194        .typescript_tsconfig_path()
195        .await?
196        .as_ref()
197        // Fall back to tsconfig only for resolving. This is because we don't want Turbopack to
198        // resolve tsconfig.json relative to the file being compiled.
199        .or(Some(&RcStr::from("tsconfig.json")))
200        .map(|p| project_path.join(p))
201        .transpose()?;
202
203    Ok(ResolveOptionsContext {
204        enable_typescript: true,
205        enable_react: true,
206        enable_mjs_extension: true,
207        custom_extensions: next_config.resolve_extension().owned().await?,
208        tsconfig_path,
209        rules: vec![(
210            foreign_code_context_condition(next_config, project_path).await?,
211            resolve_options_context.clone().resolved_cell(),
212        )],
213        ..resolve_options_context
214    }
215    .cell())
216}
217
218#[turbo_tasks::function]
219pub async fn get_client_module_options_context(
220    project_path: FileSystemPath,
221    execution_context: ResolvedVc<ExecutionContext>,
222    env: ResolvedVc<Environment>,
223    ty: ClientContextType,
224    mode: Vc<NextMode>,
225    next_config: Vc<NextConfig>,
226    encryption_key: ResolvedVc<RcStr>,
227) -> Result<Vc<ModuleOptionsContext>> {
228    let next_mode = mode.await?;
229    let resolve_options_context = get_client_resolve_options_context(
230        project_path.clone(),
231        ty.clone(),
232        mode,
233        next_config,
234        *execution_context,
235    );
236
237    let tsconfig_path = next_config
238        .typescript_tsconfig_path()
239        .await?
240        .as_ref()
241        .map(|p| project_path.join(p))
242        .transpose()?;
243
244    let tsconfig = get_typescript_transform_options(project_path.clone(), tsconfig_path.clone())
245        .to_resolved()
246        .await?;
247    let decorators_options =
248        get_decorators_transform_options(project_path.clone(), tsconfig_path.clone());
249    let enable_mdx_rs = *next_config.mdx_rs().await?;
250    let jsx_runtime_options = get_jsx_transform_options(
251        project_path.clone(),
252        mode,
253        Some(resolve_options_context),
254        false,
255        next_config,
256        tsconfig_path,
257    )
258    .to_resolved()
259    .await?;
260
261    let mut loader_conditions = BTreeSet::new();
262    loader_conditions.insert(WebpackLoaderBuiltinCondition::Browser);
263    loader_conditions.extend(mode.await?.webpack_loader_conditions());
264
265    // A separate webpack rules will be applied to codes matching foreign_code_context_condition.
266    // This allows to import codes from node_modules that requires webpack loaders, which next-dev
267    // implicitly does by default.
268    let mut foreign_conditions = loader_conditions.clone();
269    foreign_conditions.insert(WebpackLoaderBuiltinCondition::Foreign);
270    let foreign_enable_webpack_loaders =
271        *webpack_loader_options(project_path.clone(), next_config, foreign_conditions).await?;
272
273    // Now creates a webpack rules that applies to all code.
274    let enable_webpack_loaders =
275        *webpack_loader_options(project_path.clone(), next_config, loader_conditions).await?;
276
277    let tree_shaking_mode_for_user_code = *next_config
278        .tree_shaking_mode_for_user_code(next_mode.is_development())
279        .await?;
280    let tree_shaking_mode_for_foreign_code = *next_config
281        .tree_shaking_mode_for_foreign_code(next_mode.is_development())
282        .await?;
283    let target_browsers = env.runtime_versions();
284
285    let mut next_client_rules =
286        get_next_client_transforms_rules(next_config, ty.clone(), mode, false, encryption_key)
287            .await?;
288    let foreign_next_client_rules =
289        get_next_client_transforms_rules(next_config, ty.clone(), mode, true, encryption_key)
290            .await?;
291    let additional_rules: Vec<ModuleRule> = vec![
292        get_swc_ecma_transform_plugin_rule(next_config, project_path.clone()).await?,
293        get_relay_transform_rule(next_config, project_path.clone()).await?,
294        get_emotion_transform_rule(next_config).await?,
295        get_styled_components_transform_rule(next_config).await?,
296        get_styled_jsx_transform_rule(next_config, target_browsers).await?,
297        get_react_remove_properties_transform_rule(next_config).await?,
298        get_remove_console_transform_rule(next_config).await?,
299    ]
300    .into_iter()
301    .flatten()
302    .collect();
303
304    next_client_rules.extend(additional_rules);
305
306    let postcss_transform_options = PostCssTransformOptions {
307        postcss_package: Some(
308            get_postcss_package_mapping(project_path.clone())
309                .to_resolved()
310                .await?,
311        ),
312        config_location: PostCssConfigLocation::ProjectPathOrLocalPath,
313        ..Default::default()
314    };
315    let postcss_foreign_transform_options = PostCssTransformOptions {
316        // For node_modules we don't want to resolve postcss config relative to the file being
317        // compiled, instead it only uses the project root postcss config.
318        config_location: PostCssConfigLocation::ProjectPath,
319        ..postcss_transform_options.clone()
320    };
321    let enable_postcss_transform = Some(postcss_transform_options.resolved_cell());
322    let enable_foreign_postcss_transform = Some(postcss_foreign_transform_options.resolved_cell());
323
324    let source_maps = *next_config.client_source_maps(mode).await?;
325    let module_options_context = ModuleOptionsContext {
326        ecmascript: EcmascriptOptionsContext {
327            enable_typeof_window_inlining: Some(TypeofWindow::Object),
328            source_maps,
329            infer_module_side_effects: *next_config.turbopack_infer_module_side_effects().await?,
330            ..Default::default()
331        },
332        css: CssOptionsContext {
333            source_maps,
334            module_css_condition: Some(module_styles_rule_condition()),
335            ..Default::default()
336        },
337        environment: Some(env),
338        execution_context: Some(execution_context),
339        tree_shaking_mode: tree_shaking_mode_for_user_code,
340        enable_postcss_transform,
341        side_effect_free_packages: Some(
342            side_effect_free_packages_glob(next_config.optimize_package_imports())
343                .to_resolved()
344                .await?,
345        ),
346        keep_last_successful_parse: next_mode.is_development(),
347        analyze_mode: if next_mode.is_development() {
348            AnalyzeMode::CodeGeneration
349        } else {
350            // Technically, this doesn't need to tracing for the client context. But this will
351            // result in more cache hits for the analysis for modules which are loaded for both ssr
352            // and client
353            AnalyzeMode::CodeGenerationAndTracing
354        },
355        ..Default::default()
356    };
357
358    // node_modules context
359    let foreign_codes_options_context = ModuleOptionsContext {
360        ecmascript: EcmascriptOptionsContext {
361            enable_typeof_window_inlining: None,
362            // Ignore e.g. import(`${url}`) requests in node_modules.
363            ignore_dynamic_requests: true,
364            ..module_options_context.ecmascript
365        },
366        enable_webpack_loaders: foreign_enable_webpack_loaders,
367        enable_postcss_transform: enable_foreign_postcss_transform,
368        module_rules: foreign_next_client_rules,
369        tree_shaking_mode: tree_shaking_mode_for_foreign_code,
370        // NOTE(WEB-1016) PostCSS transforms should also apply to foreign code.
371        ..module_options_context.clone()
372    };
373
374    let internal_context = ModuleOptionsContext {
375        ecmascript: EcmascriptOptionsContext {
376            enable_typescript_transform: Some(
377                TypescriptTransformOptions::default().resolved_cell(),
378            ),
379            enable_jsx: Some(JsxTransformOptions::default().resolved_cell()),
380            ..module_options_context.ecmascript.clone()
381        },
382        enable_postcss_transform: None,
383        ..module_options_context.clone()
384    };
385
386    let module_options_context = ModuleOptionsContext {
387        // We don't need to resolve React Refresh for each module. Instead,
388        // we try resolve it once at the root and pass down a context to all
389        // the modules.
390        ecmascript: EcmascriptOptionsContext {
391            enable_jsx: Some(jsx_runtime_options),
392            enable_typescript_transform: Some(tsconfig),
393            enable_decorators: Some(decorators_options.to_resolved().await?),
394            ..module_options_context.ecmascript.clone()
395        },
396        enable_webpack_loaders,
397        enable_mdx_rs,
398        rules: vec![
399            (
400                foreign_code_context_condition(next_config, project_path).await?,
401                foreign_codes_options_context.resolved_cell(),
402            ),
403            (
404                internal_assets_conditions().await?,
405                internal_context.resolved_cell(),
406            ),
407        ],
408        module_rules: next_client_rules,
409        ..module_options_context
410    }
411    .cell();
412
413    Ok(module_options_context)
414}
415
416#[derive(Clone, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, Encode, Decode)]
417pub struct ClientChunkingContextOptions {
418    pub mode: Vc<NextMode>,
419    pub root_path: FileSystemPath,
420    pub client_root: FileSystemPath,
421    pub client_root_to_root_path: RcStr,
422    pub asset_prefix: Vc<RcStr>,
423    pub environment: Vc<Environment>,
424    pub module_id_strategy: Vc<Box<dyn ModuleIdStrategy>>,
425    pub export_usage: Vc<OptionBindingUsageInfo>,
426    pub unused_references: Vc<OptionBindingUsageInfo>,
427    pub minify: Vc<bool>,
428    pub source_maps: Vc<SourceMapsType>,
429    pub no_mangling: Vc<bool>,
430    pub scope_hoisting: Vc<bool>,
431    pub nested_async_chunking: Vc<bool>,
432    pub debug_ids: Vc<bool>,
433    pub should_use_absolute_url_references: Vc<bool>,
434}
435
436#[turbo_tasks::function]
437pub async fn get_client_chunking_context(
438    options: ClientChunkingContextOptions,
439) -> Result<Vc<Box<dyn ChunkingContext>>> {
440    let ClientChunkingContextOptions {
441        mode,
442        root_path,
443        client_root,
444        client_root_to_root_path,
445        asset_prefix,
446        environment,
447        module_id_strategy,
448        export_usage,
449        unused_references,
450        minify,
451        source_maps,
452        no_mangling,
453        scope_hoisting,
454        nested_async_chunking,
455        debug_ids,
456        should_use_absolute_url_references,
457    } = options;
458
459    let next_mode = mode.await?;
460    let asset_prefix = asset_prefix.owned().await?;
461    let mut builder = BrowserChunkingContext::builder(
462        root_path,
463        client_root.clone(),
464        client_root_to_root_path,
465        client_root.clone(),
466        client_root.join("static/chunks")?,
467        get_client_assets_path(client_root.clone()).owned().await?,
468        environment.to_resolved().await?,
469        next_mode.runtime_type(),
470    )
471    .chunk_base_path(Some(asset_prefix.clone()))
472    .chunk_suffix(ChunkSuffix::FromScriptSrc.resolved_cell())
473    .minify_type(if *minify.await? {
474        MinifyType::Minify {
475            mangle: (!*no_mangling.await?).then_some(MangleType::OptimalSize),
476        }
477    } else {
478        MinifyType::NoMinify
479    })
480    .source_maps(*source_maps.await?)
481    .asset_base_path(Some(asset_prefix))
482    .current_chunk_method(CurrentChunkMethod::DocumentCurrentScript)
483    .export_usage(*export_usage.await?)
484    .unused_references(*unused_references.await?)
485    .module_id_strategy(module_id_strategy.to_resolved().await?)
486    .debug_ids(*debug_ids.await?)
487    .should_use_absolute_url_references(*should_use_absolute_url_references.await?)
488    .nested_async_availability(*nested_async_chunking.await?);
489
490    if next_mode.is_development() {
491        builder = builder
492            .hot_module_replacement()
493            .source_map_source_type(SourceMapSourceType::AbsoluteFileUri)
494            .dynamic_chunk_content_loading(true);
495    } else {
496        builder = builder
497            .chunking_config(
498                Vc::<EcmascriptChunkType>::default().to_resolved().await?,
499                ChunkingConfig {
500                    min_chunk_size: 50_000,
501                    max_chunk_count_per_group: 40,
502                    max_merge_chunk_size: 200_000,
503                    ..Default::default()
504                },
505            )
506            .chunking_config(
507                Vc::<CssChunkType>::default().to_resolved().await?,
508                ChunkingConfig {
509                    max_merge_chunk_size: 100_000,
510                    ..Default::default()
511                },
512            )
513            .use_content_hashing(ContentHashing::Direct { length: 16 })
514            .module_merging(*scope_hoisting.await?);
515    }
516
517    Ok(Vc::upcast(builder.build()))
518}
519
520#[turbo_tasks::function]
521pub fn get_client_assets_path(client_root: FileSystemPath) -> Result<Vc<FileSystemPath>> {
522    Ok(client_root.join("static/media")?.cell())
523}
524
525#[turbo_tasks::function]
526pub async fn get_client_runtime_entries(
527    project_root: FileSystemPath,
528    ty: ClientContextType,
529    mode: Vc<NextMode>,
530    next_config: Vc<NextConfig>,
531    execution_context: Vc<ExecutionContext>,
532) -> Result<Vc<RuntimeEntries>> {
533    let mut runtime_entries = vec![];
534    let resolve_options_context = get_client_resolve_options_context(
535        project_root.clone(),
536        ty.clone(),
537        mode,
538        next_config,
539        execution_context,
540    );
541
542    if mode.await?.is_development() {
543        let enable_react_refresh =
544            assert_can_resolve_react_refresh(project_root.clone(), resolve_options_context)
545                .await?
546                .as_request();
547
548        // It's important that React Refresh come before the regular bootstrap file,
549        // because the bootstrap contains JSX which requires Refresh's global
550        // functions to be available.
551        if let Some(request) = enable_react_refresh {
552            runtime_entries.push(
553                RuntimeEntry::Request(request.to_resolved().await?, project_root.join("_")?)
554                    .resolved_cell(),
555            )
556        };
557    }
558
559    if matches!(ty, ClientContextType::App { .. },) {
560        runtime_entries.push(
561            RuntimeEntry::Request(
562                Request::parse(Pattern::Constant(rcstr!(
563                    "next/dist/client/app-next-turbopack.js"
564                )))
565                .to_resolved()
566                .await?,
567                project_root.join("_")?,
568            )
569            .resolved_cell(),
570        );
571    }
572
573    Ok(Vc::cell(runtime_entries))
574}