Skip to main content

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