diff --git a/packages/next-swc/crates/next-core/src/babel.rs b/packages/next-swc/crates/next-core/src/babel.rs index 96782ab744ae..17513c3f41ca 100644 --- a/packages/next-swc/crates/next-core/src/babel.rs +++ b/packages/next-swc/crates/next-core/src/babel.rs @@ -6,9 +6,10 @@ use turbo_binding::{ issue::{Issue, IssueSeverity, IssueSeverityVc, IssueVc}, resolve::{parse::RequestVc, pattern::Pattern, resolve}, }, - node::transforms::webpack::{WebpackLoaderConfigItem, WebpackLoaderConfigItemsVc}, + node::transforms::webpack::{WebpackLoaderItem, WebpackLoaderItemsVc}, turbopack::{ - module_options::WebpackLoadersOptionsVc, resolve_options, + module_options::{LoaderRuleItem, OptionWebpackRulesVc, WebpackRulesVc}, + resolve_options, resolve_options_context::ResolveOptionsContext, }, }, @@ -36,8 +37,8 @@ const BABEL_CONFIG_FILES: &[&str] = &[ #[turbo_tasks::function] pub async fn maybe_add_babel_loader( project_root: FileSystemPathVc, - webpack_options: WebpackLoadersOptionsVc, -) -> Result { + webpack_rules: Option, +) -> Result { let has_babel_config = { let mut has_babel_config = false; for filename in BABEL_CONFIG_FILES { @@ -51,30 +52,22 @@ pub async fn maybe_add_babel_loader( }; if has_babel_config { - let mut options = (*webpack_options.await?).clone(); + let mut rules = if let Some(webpack_rules) = webpack_rules { + webpack_rules.await?.clone_value() + } else { + Default::default() + }; let mut has_emitted_babel_resolve_issue = false; - for ext in [".js", ".jsx", ".ts", ".tsx", ".cjs", ".mjs"] { - let configs = options.extension_to_loaders.get(ext); - let has_babel_loader = match configs { - None => false, - Some(configs) => { - let mut has_babel_loader = false; - for config in &*configs.await? { - let name = match config { - WebpackLoaderConfigItem::LoaderName(name) => name, - WebpackLoaderConfigItem::LoaderNameWithOptions { - loader: name, - options: _, - } => name, - }; - - if name == "babel-loader" { - has_babel_loader = true; - break; - } - } - has_babel_loader - } + let mut has_changed = false; + for pattern in ["*.js", "*.jsx", "*.ts", "*.tsx", "*.cjs", "*.mjs"] { + let rule = rules.get_mut(pattern); + let has_babel_loader = if let Some(rule) = rule.as_ref() { + rule.loaders + .await? + .iter() + .any(|c| c.loader == "babel-loader") + } else { + false }; if !has_babel_loader { @@ -100,24 +93,34 @@ pub async fn maybe_add_babel_loader( has_emitted_babel_resolve_issue = true; } - let loader = WebpackLoaderConfigItem::LoaderName("babel-loader".to_owned()); - options.extension_to_loaders.insert( - ext.to_owned(), - if options.extension_to_loaders.contains_key(ext) { - let mut new_configs = (*(options.extension_to_loaders[ext].await?)).clone(); - new_configs.push(loader); - WebpackLoaderConfigItemsVc::cell(new_configs) - } else { - WebpackLoaderConfigItemsVc::cell(vec![loader]) - }, - ); + let loader = WebpackLoaderItem { + loader: "babel-loader".to_string(), + options: Default::default(), + }; + if let Some(rule) = rule { + let mut loaders = rule.loaders.await?.clone_value(); + loaders.push(loader); + rule.loaders = WebpackLoaderItemsVc::cell(loaders); + } else { + rules.insert( + pattern.to_string(), + LoaderRuleItem { + loaders: WebpackLoaderItemsVc::cell(vec![loader]), + rename_as: Some("*".to_string()), + }, + ); + } + has_changed = true; } } - Ok(options.cell()) - } else { - Ok(webpack_options) + if has_changed { + return Ok(OptionWebpackRulesVc::cell(Some(WebpackRulesVc::cell( + rules, + )))); + } } + Ok(OptionWebpackRulesVc::cell(webpack_rules)) } #[turbo_tasks::function] diff --git a/packages/next-swc/crates/next-core/src/next_client/context.rs b/packages/next-swc/crates/next-core/src/next_client/context.rs index 08ae3784ceb6..48cd47c2ba91 100644 --- a/packages/next-swc/crates/next-core/src/next_client/context.rs +++ b/packages/next-swc/crates/next-core/src/next_client/context.rs @@ -178,21 +178,17 @@ pub async fn get_client_module_options_context( let decorators_options = get_decorators_transform_options(project_path); let mdx_rs_options = *next_config.mdx_rs().await?; let jsx_runtime_options = get_jsx_transform_options(project_path); - let enable_webpack_loaders = { - let options = &*next_config.webpack_loaders_options().await?; - let loaders_options = WebpackLoadersOptions { - extension_to_loaders: options.clone(), + let webpack_rules = + *maybe_add_babel_loader(project_path, *next_config.webpack_rules().await?).await?; + let enable_webpack_loaders = webpack_rules.map(|rules| { + WebpackLoadersOptions { + rules, loader_runner_package: Some(get_external_next_compiled_package_mapping( StringVc::cell("loader-runner".to_owned()), )), - placeholder_for_future_extensions: (), } - .cell(); - - maybe_add_babel_loader(project_path, loaders_options) - .await? - .clone_if() - }; + .cell() + }); let enable_emotion = *get_emotion_compiler_config(next_config).await?; diff --git a/packages/next-swc/crates/next-core/src/next_config.rs b/packages/next-swc/crates/next-core/src/next_config.rs index a7efdf528ec9..bf3ec3229fc2 100644 --- a/packages/next-swc/crates/next-core/src/next_config.rs +++ b/packages/next-swc/crates/next-core/src/next_config.rs @@ -11,7 +11,7 @@ use turbo_binding::{ chunk::ChunkingContext, context::AssetContext, ident::AssetIdentVc, - issue::IssueContextExt, + issue::{Issue, IssueContextExt, IssueSeverity, IssueSeverityVc, IssueVc}, reference_type::{EntryReferenceSubType, ReferenceType}, resolve::{ find_context_file, @@ -27,16 +27,19 @@ use turbo_binding::{ node::{ evaluate::evaluate, execution_context::{ExecutionContext, ExecutionContextVc}, - transforms::webpack::{WebpackLoaderConfigItems, WebpackLoaderConfigItemsVc}, + transforms::webpack::{WebpackLoaderItem, WebpackLoaderItemsVc}, }, turbopack::{ evaluate_context::node_evaluate_asset_context, - module_options::StyledComponentsTransformConfig, + module_options::{ + LoaderRuleItem, OptionWebpackRulesVc, StyledComponentsTransformConfig, + WebpackRulesVc, + }, }, }, }; use turbo_tasks::{ - primitives::{BoolVc, StringsVc}, + primitives::{BoolVc, StringVc, StringsVc}, trace::TraceRawVcs, CompletionVc, Value, }; @@ -352,10 +355,30 @@ pub enum RemotePatternProtocal { #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[serde(rename_all = "camelCase")] pub struct ExperimentalTurboConfig { - pub loaders: Option>, + /// This option has been replace by `rules`. + pub loaders: Option, + pub rules: Option>, pub resolve_alias: Option>, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(rename_all = "camelCase", untagged)] +pub enum RuleConfigItem { + Loaders(Vec), + Options { + loaders: Vec, + #[serde(default, alias = "as")] + rename_as: Option, + }, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(untagged)] +pub enum LoaderItem { + LoaderName(String), + LoaderOptions(WebpackLoaderItem), +} + #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[serde(rename_all = "camelCase")] pub struct ExperimentalConfig { @@ -477,10 +500,6 @@ pub enum RemoveConsoleConfig { Config { exclude: Option> }, } -#[derive(Default)] -#[turbo_tasks::value(transparent)] -pub struct WebpackExtensionToLoaders(IndexMap); - #[turbo_tasks::value_impl] impl NextConfigVc { #[turbo_tasks::function] @@ -555,19 +574,46 @@ impl NextConfigVc { } #[turbo_tasks::function] - pub async fn webpack_loaders_options(self) -> Result { + pub async fn webpack_rules(self) -> Result { let this = self.await?; - let Some(turbo_loaders) = this.experimental.turbo.as_ref().and_then(|t| t.loaders.as_ref()) else { - return Ok(WebpackExtensionToLoadersVc::cell(IndexMap::new())); + let Some(turbo_rules) = this.experimental.turbo.as_ref().and_then(|t| t.rules.as_ref()) else { + return Ok(OptionWebpackRulesVc::cell(None)); }; - let mut extension_to_loaders = IndexMap::new(); - for (ext, loaders) in turbo_loaders { - extension_to_loaders.insert( - ext.clone(), - WebpackLoaderConfigItemsVc::cell(loaders.0.clone()), - ); + if turbo_rules.is_empty() { + return Ok(OptionWebpackRulesVc::cell(None)); + } + let mut rules = IndexMap::new(); + for (ext, rule) in turbo_rules { + fn transform_loaders(loaders: &[LoaderItem]) -> WebpackLoaderItemsVc { + WebpackLoaderItemsVc::cell( + loaders + .iter() + .map(|item| match item { + LoaderItem::LoaderName(name) => WebpackLoaderItem { + loader: name.clone(), + options: Default::default(), + }, + LoaderItem::LoaderOptions(options) => options.clone(), + }) + .collect(), + ) + } + let rule = match rule { + RuleConfigItem::Loaders(loaders) => LoaderRuleItem { + loaders: transform_loaders(loaders), + rename_as: None, + }, + RuleConfigItem::Options { loaders, rename_as } => LoaderRuleItem { + loaders: transform_loaders(loaders), + rename_as: rename_as.clone(), + }, + }; + + rules.insert(ext.clone(), rule); } - Ok(WebpackExtensionToLoaders(extension_to_loaders).cell()) + Ok(OptionWebpackRulesVc::cell(Some(WebpackRulesVc::cell( + rules, + )))) } #[turbo_tasks::function] @@ -667,6 +713,23 @@ pub async fn load_next_config_internal( }; let next_config: NextConfig = parse_json_with_source_context(val.to_str()?)?; + if let Some(turbo) = next_config.experimental.turbo.as_ref() { + if turbo.loaders.is_some() { + OutdatedConfigIssue { + path: config_file.unwrap_or(project_path), + old_name: "experimental.turbo.loaders".to_string(), + new_name: "experimental.turbo.rules".to_string(), + description: "The new option is similar, but the key should be a glob instead of \ + an extension. +Example: loaders: { \".mdx\": [\"mdx-loader\"] } -> rules: { \"*.mdx\": [\"mdx-loader\"] }" + .to_string(), + } + .cell() + .as_issue() + .emit() + } + } + Ok(next_config.cell()) } @@ -677,3 +740,42 @@ pub async fn has_next_config(context: FileSystemPathVc) -> Result { FindContextFileResult::NotFound(_) ))) } + +#[turbo_tasks::value] +struct OutdatedConfigIssue { + path: FileSystemPathVc, + old_name: String, + new_name: String, + description: String, +} + +#[turbo_tasks::value_impl] +impl Issue for OutdatedConfigIssue { + #[turbo_tasks::function] + fn severity(&self) -> IssueSeverityVc { + IssueSeverity::Error.into() + } + + #[turbo_tasks::function] + fn category(&self) -> StringVc { + StringVc::cell("config".to_string()) + } + + #[turbo_tasks::function] + fn context(&self) -> FileSystemPathVc { + self.path + } + + #[turbo_tasks::function] + fn title(&self) -> StringVc { + StringVc::cell(format!( + "\"{}\" has been replaced by \"{}\"", + self.old_name, self.new_name + )) + } + + #[turbo_tasks::function] + fn description(&self) -> StringVc { + StringVc::cell(self.description.to_string()) + } +} diff --git a/packages/next-swc/crates/next-core/src/next_server/context.rs b/packages/next-swc/crates/next-core/src/next_server/context.rs index ff1231602f47..8dd588348afa 100644 --- a/packages/next-swc/crates/next-core/src/next_server/context.rs +++ b/packages/next-swc/crates/next-core/src/next_server/context.rs @@ -261,21 +261,17 @@ pub async fn get_server_module_options_context( ..Default::default() }); - let enable_webpack_loaders = { - let options = &*next_config.webpack_loaders_options().await?; - let loaders_options = WebpackLoadersOptions { - extension_to_loaders: options.clone(), + let webpack_rules = + *maybe_add_babel_loader(project_path, *next_config.webpack_rules().await?).await?; + let enable_webpack_loaders = webpack_rules.map(|rules| { + WebpackLoadersOptions { + rules: rules.clone(), loader_runner_package: Some(get_external_next_compiled_package_mapping( StringVc::cell("loader-runner".to_owned()), )), - placeholder_for_future_extensions: (), } - .cell(); - - maybe_add_babel_loader(project_path, loaders_options) - .await? - .clone_if() - }; + .cell() + }); let tsconfig = get_typescript_transform_options(project_path); let decorators_options = get_decorators_transform_options(project_path); diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/error/webpack-loaders/input/next.config.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/error/webpack-loaders/input/next.config.js new file mode 100644 index 000000000000..c57d50163ebe --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/error/webpack-loaders/input/next.config.js @@ -0,0 +1,11 @@ +module.exports = { + experimental: { + turbo: { + loaders: { + '.replace': [ + { loader: 'replace-loader', options: { defaultExport: 3 } }, + ], + }, + }, + }, +} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/error/webpack-loaders/input/pages/index.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/error/webpack-loaders/input/pages/index.js new file mode 100644 index 000000000000..060471b4b150 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/error/webpack-loaders/input/pages/index.js @@ -0,0 +1,11 @@ +import { useTestHarness } from '@turbo/pack-test-harness' + +export default function Home() { + useTestHarness(runTests) + + return null +} + +function runTests() { + it('run', () => {}) +} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/error/webpack-loaders/issues/__quo__experimental.turbo.loaders__quo__ has been -75c95a.txt b/packages/next-swc/crates/next-dev-tests/tests/integration/next/error/webpack-loaders/issues/__quo__experimental.turbo.loaders__quo__ has been -75c95a.txt new file mode 100644 index 000000000000..69a55cc2aed4 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/error/webpack-loaders/issues/__quo__experimental.turbo.loaders__quo__ has been -75c95a.txt @@ -0,0 +1,21 @@ +PlainIssue { + severity: Error, + context: "[project]/packages/next-swc/crates/next-dev-tests/tests/integration/next/error/webpack-loaders/input/next.config.js", + category: "config", + title: "\"experimental.turbo.loaders\" has been replaced by \"experimental.turbo.rules\"", + description: "The new option is similar, but the key should be a glob instead of an extension.\nExample: loaders: { \".mdx\": [\"mdx-loader\"] } -> rules: { \"*.mdx\": [\"mdx-loader\"] }", + detail: "", + documentation_link: "", + source: None, + sub_issues: [], + processing_path: Some( + [ + PlainIssueProcessingPathItem { + context: Some( + "[project]/packages/next-swc/crates/next-dev-tests/tests/integration/next/error/webpack-loaders/input/next.config.js", + ), + description: "Loading Next.js config", + }, + ], + ), +} \ No newline at end of file diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/next.config.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/next.config.js index c57d50163ebe..34e8968264d8 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/next.config.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/next.config.js @@ -1,8 +1,8 @@ module.exports = { experimental: { turbo: { - loaders: { - '.replace': [ + rules: { + '*.replace': [ { loader: 'replace-loader', options: { defaultExport: 3 } }, ], }, diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/emitted-errors/input/next.config.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/emitted-errors/input/next.config.js index 0e34aefe9786..73a765218b60 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/emitted-errors/input/next.config.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/emitted-errors/input/next.config.js @@ -1,8 +1,8 @@ module.exports = { experimental: { turbo: { - loaders: { - '.emit': ['emit-loader'], + rules: { + '*.emit': ['emit-loader'], }, }, }, diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/next.config.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/next.config.js index feacb3f1225b..3509d669a05d 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/next.config.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/next.config.js @@ -1,8 +1,17 @@ module.exports = { experimental: { turbo: { - loaders: { - '.raw': ['raw-loader'], + rules: { + '*.raw.*': { + loaders: ['raw-loader'], + as: "*" + }, + '*.raw': ['raw-loader'], + '*.jraw': { + loaders: ['raw-as-json-loader'], + as: "*.json" + }, + './raw/**': ['raw-loader'], }, }, }, diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/node_modules/raw-as-json-loader/index.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/node_modules/raw-as-json-loader/index.js new file mode 100644 index 000000000000..422794b59f71 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/node_modules/raw-as-json-loader/index.js @@ -0,0 +1,3 @@ +module.exports = async (source) => { + return `${JSON.stringify(source.trim())}` +} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/hello.jraw b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/hello.jraw new file mode 100644 index 000000000000..557db03de997 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/hello.jraw @@ -0,0 +1 @@ +Hello World diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/hello.raw.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/hello.raw.js new file mode 100644 index 000000000000..557db03de997 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/hello.raw.js @@ -0,0 +1 @@ +Hello World diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/hello.raw.raw b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/hello.raw.raw new file mode 100644 index 000000000000..557db03de997 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/hello.raw.raw @@ -0,0 +1 @@ +Hello World diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/index.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/index.js index eaacde584940..086d7b3ba45f 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/index.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/index.js @@ -1,5 +1,9 @@ import { useTestHarness } from '@turbo/pack-test-harness' -import source from './hello.raw' +import source1 from './hello.raw' +import source2 from '../raw/hello.js' +import source3 from './hello.raw.raw' +import source4 from './hello.raw.js' +import source5 from './hello.jraw' export default function Home() { useTestHarness(runTests) @@ -9,6 +13,18 @@ export default function Home() { function runTests() { it('runs a simple loader', () => { - expect(source).toBe('Hello World') + expect(source1).toBe('Hello World') + }) + it('runs a loader matching relative path glob', () => { + expect(source2).toBe('}}} Hello World') + }) + it('runs a loader with "as" continue (to other match)', () => { + expect(source3).toBe('export default \"Hello World\";') + }) + it('runs a loader with "as" continue (to default js match)', () => { + expect(source4).toBe('Hello World') + }) + it('runs a loader with "as" to builtin type', () => { + expect(source5).toBe('Hello World') }) } diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/raw/hello.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/raw/hello.js new file mode 100644 index 000000000000..1b617ed34e23 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/raw/hello.js @@ -0,0 +1 @@ +}}} Hello World \ No newline at end of file