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