diff --git a/Cargo.lock b/Cargo.lock index 94fcbc7be2a58f..bfabf9fd83cc3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3512,10 +3512,13 @@ version = "0.1.0" dependencies = [ "anyhow", "auto-hash-map", + "crossterm", "indexmap", "indoc", "mime", + "num_cpus", "once_cell", + "owo-colors", "qstring", "serde", "serde_json", diff --git a/crates/next-core/Cargo.toml b/crates/next-core/Cargo.toml index 19eec7b4ba5f60..e61cf8e9381845 100644 --- a/crates/next-core/Cargo.toml +++ b/crates/next-core/Cargo.toml @@ -11,10 +11,13 @@ bench = false [dependencies] anyhow = "1.0.47" auto-hash-map = { path = "../auto-hash-map" } +crossterm = "0.25" indexmap = { workspace = true, features = ["serde"] } indoc = "1.0" mime = "0.3.16" +num_cpus = "1.13.1" once_cell = "1.13.0" +owo-colors = "3" qstring = "0.7.2" serde = "1.0.136" serde_json = "1.0.85" diff --git a/crates/next-core/js/src/entry/config/next.js b/crates/next-core/js/src/entry/config/next.js index 349540a9ca4c20..52d2e3b8fbc283 100644 --- a/crates/next-core/js/src/entry/config/next.js +++ b/crates/next-core/js/src/entry/config/next.js @@ -3,8 +3,16 @@ import { PHASE_DEVELOPMENT_SERVER } from "next/dist/shared/lib/constants"; const loadNextConfig = async () => { const nextConfig = await loadConfig(PHASE_DEVELOPMENT_SERVER, process.cwd()); - nextConfig.rewrites = await nextConfig.rewrites?.(); + + nextConfig.generateBuildId = await nextConfig.generateBuildId?.(); + nextConfig.headers = await nextConfig.headers?.(); nextConfig.redirects = await nextConfig.redirects?.(); + nextConfig.rewrites = await nextConfig.rewrites?.(); + + // TODO: these functions takes arguments, have to be supported in a different way + nextConfig.exportPathMap = nextConfig.exportPathMap && {}; + nextConfig.webpack = nextConfig.webpack && {}; + return nextConfig; }; diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index 91b38748338325..94f822ac701649 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -1,12 +1,14 @@ -use std::collections::HashMap; +use std::{collections::HashMap, default::Default, fmt::Write}; -use anyhow::Result; +use anyhow::{bail, Result}; +use crossterm::style::Stylize; use serde::{Deserialize, Serialize}; use turbo_tasks::{ primitives::{BoolVc, StringsVc}, trace::TraceRawVcs, Value, }; +use turbo_tasks_fs::{FileSystemEntryType, FileSystemPathVc}; use turbopack::evaluate_context::node_evaluate_asset_context; use turbopack_core::{ asset::Asset, @@ -29,29 +31,212 @@ use turbopack_node::{ use crate::embed_js::next_asset; -#[turbo_tasks::value(serialization = "custom")] -#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[turbo_tasks::value(serialization = "custom", eq = "manual")] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NextConfig { - pub config_file: Option, + pub cross_origin: Option, + pub config_file: String, pub config_file_name: String, - pub typescript: Option, + pub react_strict_mode: Option, - pub experimental: Option, - pub env: Option>, - pub compiler: Option, + pub experimental: ExperimentalConfig, pub images: ImageConfig, + + #[serde(flatten)] + unsupported: UnsupportedNextConfig, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "camelCase")] +struct AmpConfig { + canonical_base: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "camelCase")] +struct EslintConfig { + dirs: Option>, + ignore_during_builds: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "kebab-case")] +enum BuildActivityPositions { + BottomRight, + BottomLeft, + TopRight, + TopLeft, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "camelCase")] +struct DevIndicatorsConfig { + build_activity: bool, + build_activity_position: BuildActivityPositions, } -#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "camelCase")] +struct OnDemandEntriesConfig { + max_inactive_age: f64, + pages_buffer_length: f64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "camelCase")] +struct HttpAgentConfig { + keep_alive: bool, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "camelCase")] +struct DomainLocale { + default_locale: String, + domain: String, + http: Option, + locales: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "camelCase")] +struct I18NConfig { + default_locale: String, + domains: Option>, + locale_detection: Option, + locales: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "kebab-case")] +enum OutputType { + Standalone, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[serde(rename_all = "camelCase")] pub struct TypeScriptConfig { pub ignore_build_errors: Option, pub ts_config_path: Option, } -#[turbo_tasks::value] -#[derive(Clone, Debug, Ord, PartialOrd)] +/// unsupported next config options +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "camelCase")] +struct UnsupportedNextConfig { + amp: AmpConfig, + analytics_id: String, + asset_prefix: String, + base_path: String, + clean_dist_dir: bool, + compiler: Option, + compress: bool, + dev_indicators: DevIndicatorsConfig, + dist_dir: String, + env: HashMap, + eslint: EslintConfig, + exclude_default_moment_locales: bool, + // this can be a function in js land? + export_path_map: Option, + // this is a function in js land? + generate_build_id: Option, + generate_etags: bool, + // this is a function in js land? + headers: Option, + http_agent_options: HttpAgentConfig, + i18n: Option, + on_demand_entries: OnDemandEntriesConfig, + optimize_fonts: bool, + output: Option, + output_file_tracing: bool, + page_extensions: Vec, + powered_by_header: bool, + production_browser_source_maps: bool, + public_runtime_config: HashMap, + react_strict_mode: Option, + // this is a function in js land? + redirects: Option, + // this is a function in js land? + rewrites: Option, + sass_options: HashMap, + server_runtime_config: HashMap, + static_page_generation_timeout: f64, + swc_minify: bool, + target: String, + trailing_slash: bool, + typescript: TypeScriptConfig, + use_file_system_public_routes: bool, + webpack: Option, +} + +impl Default for UnsupportedNextConfig { + fn default() -> Self { + UnsupportedNextConfig { + amp: AmpConfig { + canonical_base: Some("".to_string()), + }, + // process.env.VERCEL_ANALYTICS_ID || '', + analytics_id: "".to_string(), + asset_prefix: "".to_string(), + base_path: "".to_string(), + clean_dist_dir: true, + compiler: None, + compress: true, + dev_indicators: DevIndicatorsConfig { + build_activity: true, + build_activity_position: BuildActivityPositions::BottomRight, + }, + dist_dir: ".next".to_string(), + env: Default::default(), + eslint: EslintConfig { + dirs: None, + ignore_during_builds: Some(false), + }, + exclude_default_moment_locales: true, + export_path_map: None, + generate_build_id: None, + generate_etags: true, + headers: None, + http_agent_options: HttpAgentConfig { keep_alive: true }, + i18n: None, + on_demand_entries: OnDemandEntriesConfig { + max_inactive_age: 15.0 * 1000.0, + pages_buffer_length: 2.0, + }, + optimize_fonts: true, + // !!process.env.NEXT_PRIVATE_STANDALONE ? 'standalone' : undefined + output: None, + output_file_tracing: true, + page_extensions: vec![ + "tsx".to_string(), + "ts".to_string(), + "jsx".to_string(), + "js".to_string(), + ], + powered_by_header: true, + production_browser_source_maps: false, + public_runtime_config: Default::default(), + react_strict_mode: None, + redirects: None, + rewrites: None, + sass_options: Default::default(), + server_runtime_config: Default::default(), + static_page_generation_timeout: 60.0, + swc_minify: true, + target: "server".to_string(), + trailing_slash: false, + typescript: TypeScriptConfig { + ignore_build_errors: Some(false), + ts_config_path: None, + }, + use_file_system_public_routes: true, + webpack: None, + } + } +} + +#[turbo_tasks::value(eq = "manual")] +#[derive(Clone, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ImageConfig { pub device_sizes: Vec, @@ -90,8 +275,8 @@ impl Default for ImageConfig { } } -#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] -#[serde(rename_all = "lowercase")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "kebab-case")] pub enum ImageLoader { Default, Imgix, @@ -100,17 +285,15 @@ pub enum ImageLoader { Custom, } -#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] pub enum ImageFormat { - #[serde(rename(deserialize = "image/webp"))] + #[serde(rename = "image/webp")] Webp, - #[serde(rename(deserialize = "image/avif"))] + #[serde(rename = "image/avif")] Avif, } -#[derive( - Clone, Debug, Default, Ord, PartialOrd, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, -)] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[serde(rename_all = "camelCase")] pub struct RemotePattern { pub protocol: Option, @@ -119,21 +302,155 @@ pub struct RemotePattern { pub pathname: Option, } -#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] -#[serde(rename_all = "lowercase")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "kebab-case")] pub enum RemotePatternProtocal { Http, Https, } -#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[serde(rename_all = "camelCase")] pub struct ExperimentalConfig { pub server_components_external_packages: Option>, pub app_dir: Option, + + // we only have swc? + force_swc_transforms: Option, + + // supported? + transpile_packages: Option>, + + #[serde(flatten)] + unsupported: UnsupportedExperimentalConfig, } -#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "kebab-case")] +enum MiddlewarePrefetchType { + Strict, + Flexible, +} + +/// unsupported experimental config options +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "camelCase")] +struct UnsupportedExperimentalConfig { + adjust_font_fallbacks: Option, + adjust_font_fallbacks_with_size_adjust: Option, + allow_middleware_response_body: Option, + amp: Option, + cpus: Option, + cra_compat: Option, + disable_optimized_loading: Option, + disable_postcss_preset_env: Option, + enable_undici: Option, + esm_externals: Option, + external_dir: Option, + fallback_node_polyfills: Option, + fetch_cache: Option, + font_loaders: Option, + fully_specified: Option, + gzip_size: Option, + incremental_cache_handler_path: Option, + isr_flush_to_disk: Option, + isr_memory_cache_size: Option, + large_page_data_bytes: Option, + legacy_browsers: Option, + manual_client_base_path: Option, + mdx_rs: Option, + middleware_prefetch: Option, + modularize_imports: Option, + new_next_link_behavior: Option, + next_script_workers: Option, + optimistic_client_cache: Option, + optimize_css: Option, + output_file_tracing_ignores: Option>, + output_file_tracing_root: Option, + page_env: Option, + profiling: Option, + proxy_timeout: Option, + runtime: Option, + scroll_restoration: Option, + shared_pool: Option, + skip_middleware_url_normalize: Option, + skip_trailing_slash_redirect: Option, + sri: Option, + swc_file_reading: Option, + swc_minify: Option, + swc_minify_debug_options: Option, + swc_plugins: Option, + swc_trace_profiling: Option, + turbotrace: Option, + url_imports: Option, + web_vitals_attribution: Option, + worker_threads: Option, +} + +impl Default for UnsupportedExperimentalConfig { + fn default() -> Self { + UnsupportedExperimentalConfig { + adjust_font_fallbacks: Some(false), + adjust_font_fallbacks_with_size_adjust: Some(false), + allow_middleware_response_body: None, + amp: None, + // cpus: Math.max( + // 1, + // (Number(process.env.CIRCLE_NODE_TOTAL) || + // (os.cpus() || { length: 1 }).length) - 1 + // ), + cpus: Some(f64::max(1.0, num_cpus::get() as f64 - 1.0)), + cra_compat: Some(false), + disable_optimized_loading: Some(false), + disable_postcss_preset_env: None, + enable_undici: Some(false), + esm_externals: Some(serde_json::Value::Bool(true)), + external_dir: Some(false), + fallback_node_polyfills: None, + fetch_cache: Some(false), + font_loaders: None, + fully_specified: Some(false), + gzip_size: Some(true), + incremental_cache_handler_path: None, + isr_flush_to_disk: Some(true), + // default to 50MB limit + isr_memory_cache_size: Some(50.0 * 1024.0 * 1024.0), + // default to 128KB + large_page_data_bytes: Some(128.0 * 1000.0), + legacy_browsers: Some(false), + manual_client_base_path: Some(false), + mdx_rs: None, + middleware_prefetch: Some(MiddlewarePrefetchType::Flexible), + modularize_imports: None, + new_next_link_behavior: Some(true), + next_script_workers: Some(false), + optimistic_client_cache: Some(true), + optimize_css: Some(serde_json::Value::Bool(false)), + output_file_tracing_ignores: None, + output_file_tracing_root: Some("".to_string()), + page_env: Some(false), + profiling: Some(false), + proxy_timeout: None, + runtime: None, + scroll_restoration: Some(false), + shared_pool: Some(true), + skip_middleware_url_normalize: None, + skip_trailing_slash_redirect: None, + sri: None, + swc_file_reading: Some(true), + swc_minify: None, + swc_minify_debug_options: None, + swc_plugins: None, + swc_trace_profiling: Some(false), + turbotrace: None, + url_imports: None, + web_vitals_attribution: None, + worker_threads: Some(false), + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[serde(rename_all = "camelCase")] pub struct CompilerConfig { pub react_remove_properties: Option, @@ -141,14 +458,14 @@ pub struct CompilerConfig { pub remove_console: Option, } -#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[serde(untagged, rename_all = "camelCase")] pub enum ReactRemoveProperties { Boolean(bool), Config { properties: Option> }, } -#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[serde(rename_all = "camelCase")] pub struct RelayConfig { pub src: String, @@ -156,7 +473,7 @@ pub struct RelayConfig { pub language: Option, } -#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[serde(untagged, rename_all = "lowercase")] pub enum RelayLanguage { TypeScript, @@ -164,7 +481,7 @@ pub enum RelayLanguage { JavaScript, } -#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[serde(untagged)] pub enum RemoveConsoleConfig { Boolean(bool), @@ -178,8 +495,8 @@ impl NextConfigVc { Ok(StringsVc::cell( self.await? .experimental + .server_components_external_packages .as_ref() - .and_then(|e| e.server_components_external_packages.as_ref()) .cloned() .unwrap_or_default(), )) @@ -190,8 +507,8 @@ impl NextConfigVc { Ok(BoolVc::cell( self.await? .experimental + .app_dir .as_ref() - .and_then(|e| e.app_dir.as_ref()) .cloned() .unwrap_or_default(), )) @@ -263,7 +580,11 @@ pub async fn load_next_config(execution_context: ExecutionContextVc) -> Result { let next_config: NextConfig = serde_json::from_reader(val.read())?; - Ok(next_config.cell()) + let next_config = next_config.cell(); + + validate_next_config(project_root, next_config).await?; + + Ok(next_config) } JavaScriptValue::Error => Ok(NextConfig::default().cell()), JavaScriptValue::Stream(_) => { @@ -271,3 +592,178 @@ pub async fn load_next_config(execution_context: ExecutionContextVc) -> Result Result> { + for filename in BABEL_CONFIG_FILES { + let file_path = project_path.join(filename); + let file_type = file_path.get_type().await?; + + if *file_type == FileSystemEntryType::File { + return Ok(Some(file_path)); + } + } + + Ok(None) +} + +fn compare_to_default(unsupported: &T) -> Result> { + let default_cfg = T::default(); + + if *unsupported == default_cfg { + return Ok(vec![]); + } + + let value = serde_json::to_value(unsupported)?; + let default_value = serde_json::to_value(&default_cfg)?; + + let serde_json::Value::Object(map) = value else { + bail!("can only compare maps"); + }; + let serde_json::Value::Object(default_map) = default_value else { + bail!("can only compare maps"); + }; + + let mut defined_options = vec![]; + for (k, v) in map.iter() { + if default_map[k] != *v { + defined_options.push(k.clone()); + } + } + + Ok(defined_options) +} + +const SUPPORTED_OPTIONS: &[&'static str] = &[ + "images", + "reactStrictMode", + "configFileName", + "swcMinify", + "experimental.appDir", + "experimental.serverComponentsExternalPackages", +]; + +pub async fn validate_next_config( + project_path: FileSystemPathVc, + next_config: NextConfigVc, +) -> Result<()> { + let babelrc = get_babel_config_file(project_path).await?; + + let thank_you_msg = "\nThank you for trying Next.js v13 with Turbopack! As a \ + reminder,\nTurbopack is currently in alpha and not yet ready for \ + production.\nWe appreciate your ongoing support as we work to make it \ + ready\nfor everyone.\n"; + + let mut unsupported_parts = String::new(); + + let next_config = &*next_config.await?; + + let mut unsupported_options = compare_to_default(&next_config.unsupported)?; + let unsupported_experimental_options = + compare_to_default(&next_config.experimental.unsupported)?; + + for unsupported_option in unsupported_experimental_options { + unsupported_options.push(format!("experimental.{}", unsupported_option)); + } + + let has_non_default_config = unsupported_options.len() != 0; + + let has_warning_or_error = babelrc.is_some() || has_non_default_config; + if has_warning_or_error { + println!("{}", thank_you_msg); + } else { + println!("{}", thank_you_msg.dim()); + } + + let mut feedback_message = format!( + "Learn more about Next.js v13 and Turbopack: {}\nPlease direct feedback to: {}\n", + "https://nextjs.link/with-turbopack".underlined(), + "https://nextjs.link/turbopack-feedback".underlined() + ) + .stylize(); + + if !has_warning_or_error { + feedback_message = feedback_message.dim() + } + + if let Some(babelrc) = babelrc { + write!( + unsupported_parts, + "\n- Babel detected ({})\n {}", + babelrc.await?.file_name().cyan(), + "Babel is not yet supported. To use Turbopack at the moment,\n you'll need to remove \ + your usage of Babel." + .dim() + )?; + } + + if has_non_default_config { + writeln!( + unsupported_parts, + "\n- Unsupported Next.js configuration option(s) ({}):", + "next.config.js".cyan(), + )?; + + for option in unsupported_options { + writeln!(unsupported_parts, " - {}", option.red())?; + } + + writeln!( + unsupported_parts, + "{}", + " To use Turbopack, remove those configuration options.\n The only supported \ + options are:" + .dim() + )?; + + for option in SUPPORTED_OPTIONS { + writeln!(unsupported_parts, " - {}", option.cyan().dim())?; + } + } + + if !unsupported_parts.is_empty() { + // const pkgManager = getPkgManager(dir) + let pkg_manager = "npm"; + + let commands = format!( + "{}\n cd with-turbopack-app\n {pkg_manager} run dev\n", + format!( + "{} --example with-turbopack with-turbopack-app", + if pkg_manager == "npm" { + "npx create-next-app".to_string() + } else { + format!("{pkg_manager} create next-app") + } + ) + .bold() + .cyan(), + ); + + println!( + "{} You are using configuration and/or tools that are not yet\nsupported by Next.js \ + v13 with Turbopack:\n{unsupported_parts}\nIf you cannot make the changes above, but \ + still want to try out\nNext.js v13 with Turbopack, create the Next.js v13 playground \ + app\nby running the following commands:\n\n {}", + "Error:".bold().red(), + commands, + ); + println!("{}", feedback_message); + + std::process::exit(1); + } + + println!("{}", feedback_message); + + Ok(()) +} diff --git a/crates/turbo-tasks/src/trace.rs b/crates/turbo-tasks/src/trace.rs index a44d624966b233..518031e7604fcb 100644 --- a/crates/turbo-tasks/src/trace.rs +++ b/crates/turbo-tasks/src/trace.rs @@ -57,7 +57,7 @@ macro_rules! ignore { } } -ignore!(i8, u8, i16, u16, i32, u32, i64, u64, char, bool, usize); +ignore!(i8, u8, i16, u16, i32, u32, i64, u64, f32, f64, char, bool, usize); ignore!( AtomicI8, AtomicU8,