next_core/next_client/
context.rs

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