diff --git a/docs/02-app/02-api-reference/05-next-config-js/turbo.mdx b/docs/02-app/02-api-reference/05-next-config-js/turbo.mdx index 09c3044d26b90..d752e14cbbdfb 100644 --- a/docs/02-app/02-api-reference/05-next-config-js/turbo.mdx +++ b/docs/02-app/02-api-reference/05-next-config-js/turbo.mdx @@ -18,9 +18,9 @@ To configure loaders, add the names of the loaders you've installed and any opti module.exports = { experimental: { turbo: { - loaders: { + rules: { // Option format - '.md': [ + '*.md': [ { loader: '@mdx-js/loader', options: { @@ -29,7 +29,7 @@ module.exports = { }, ], // Option-less format - '.mdx': ['@mdx-js/loader'], + '*.mdx': ['@mdx-js/loader'], }, }, }, diff --git a/examples/with-turbopack-loaders/next.config.js b/examples/with-turbopack-loaders/next.config.js index 4b89c10aa7529..5ba0898344f7c 100644 --- a/examples/with-turbopack-loaders/next.config.js +++ b/examples/with-turbopack-loaders/next.config.js @@ -1,8 +1,15 @@ module.exports = { experimental: { turbo: { - loaders: { - '.svg': ['@svgr/webpack'], + rules: { + '*.react.svg': { + loaders: ['@svgr/webpack'], + as: '*.js', + }, + '*.styl': { + loaders: ['stylus-loader'], + as: '*.css', + }, }, }, }, diff --git a/examples/with-turbopack-loaders/package.json b/examples/with-turbopack-loaders/package.json index c70cb28065381..f0590fa8ad5af 100644 --- a/examples/with-turbopack-loaders/package.json +++ b/examples/with-turbopack-loaders/package.json @@ -15,6 +15,8 @@ "@types/node": "^18.11.9", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.9", + "stylus": "0.59.0", + "stylus-loader": "7.1.3", "typescript": "^4.9.3" } } diff --git a/examples/with-turbopack-loaders/pages/index.js b/examples/with-turbopack-loaders/pages/index.js index 574261858ff39..cf2f400b782ae 100644 --- a/examples/with-turbopack-loaders/pages/index.js +++ b/examples/with-turbopack-loaders/pages/index.js @@ -1,4 +1,5 @@ -import Vercel from '../vercel.svg' +import Vercel from '../vercel.react.svg' +import '../styles.styl' export default function Home() { return diff --git a/examples/with-turbopack-loaders/styles.styl b/examples/with-turbopack-loaders/styles.styl new file mode 100644 index 0000000000000..9e8b98a7a5f09 --- /dev/null +++ b/examples/with-turbopack-loaders/styles.styl @@ -0,0 +1,2 @@ +body + background-color: blue diff --git a/examples/with-turbopack-loaders/vercel.svg b/examples/with-turbopack-loaders/vercel.react.svg similarity index 100% rename from examples/with-turbopack-loaders/vercel.svg rename to examples/with-turbopack-loaders/vercel.react.svg 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 b0521dc335a2e..6457c56693a6a 100644 --- a/packages/next-swc/crates/next-core/src/next_config.rs +++ b/packages/next-swc/crates/next-core/src/next_config.rs @@ -376,7 +376,7 @@ pub enum RemotePatternProtocal { #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[serde(rename_all = "camelCase")] pub struct ExperimentalTurboConfig { - /// This option has been replace by `rules`. + /// This option has been replaced by `rules`. pub loaders: Option, pub rules: Option>, pub resolve_alias: Option>, diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 80164d4baf03f..c5d1a4ec1a652 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -8,7 +8,7 @@ import { getParserOptions } from './options' import { eventSwcLoadFailure } from '../../telemetry/events/swc-load-failure' import { patchIncorrectLockfile } from '../../lib/patch-incorrect-lockfile' import { downloadWasmSwc, downloadNativeNextSwc } from '../../lib/download-swc' -import { NextConfigComplete, TurboLoaderItem } from '../../server/config-shared' +import { NextConfigComplete, TurboRule } from '../../server/config-shared' import { isDeepStrictEqual } from 'util' const nextVersion = process.env.__NEXT_VERSION as string @@ -948,10 +948,8 @@ function bindingToApi(binding: any, _wasm: boolean) { nextConfigSerializable.exportPathMap = {} nextConfigSerializable.webpack = nextConfig.webpack && {} - if (nextConfig.experimental?.turbo?.loaders) { - ensureLoadersHaveSerializableOptions( - nextConfig.experimental.turbo.loaders - ) + if (nextConfig.experimental?.turbo?.rules) { + ensureLoadersHaveSerializableOptions(nextConfig.experimental.turbo?.rules) } nextConfigSerializable.modularizeImports = @@ -979,16 +977,17 @@ function bindingToApi(binding: any, _wasm: boolean) { } function ensureLoadersHaveSerializableOptions( - turbopackLoaders: Record + turbopackRules: Record ) { - for (const [ext, loaderItems] of Object.entries(turbopackLoaders)) { + for (const [glob, rule] of Object.entries(turbopackRules)) { + const loaderItems = Array.isArray(rule) ? rule : rule.loaders for (const loaderItem of loaderItems) { if ( typeof loaderItem !== 'string' && !isDeepStrictEqual(loaderItem, JSON.parse(JSON.stringify(loaderItem))) ) { throw new Error( - `loader ${loaderItem.loader} for match "${ext}" does not have serializable options. Ensure that options passed are plain JavaScript objects and values.` + `loader ${loaderItem.loader} for match "${glob}" does not have serializable options. Ensure that options passed are plain JavaScript objects and values.` ) } } diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 5b7b5ae9f1b58..5518ea229175c 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -453,6 +453,9 @@ const configSchema = { loaders: { type: 'object', }, + rules: { + type: 'object', + }, resolveAlias: { type: 'object', }, diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index a44d576098143..edb1e775dc013 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -93,6 +93,13 @@ export type TurboLoaderItem = options: Record } +export type TurboRule = + | TurboLoaderItem[] + | { + loaders: TurboLoaderItem[] + as: string + } + export interface ExperimentalTurboOptions { /** * (`next --turbo` only) A mapping of aliased imports to modules to load in their place. @@ -110,6 +117,13 @@ export interface ExperimentalTurboOptions { * @see [Turbopack Loaders](https://nextjs.org/docs/app/api-reference/next-config-js/turbo#webpack-loaders) */ loaders?: Record + + /** + * (`next --turbo` only) A list of webpack loaders to apply when running with Turbopack. + * + * @see [Turbopack Loaders](https://nextjs.org/docs/app/api-reference/next-config-js/turbo#webpack-loaders) + */ + rules?: Record } export interface WebpackConfigContext { diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 9c08a33db917e..9a593d95186a5 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -12,6 +12,7 @@ import { NextConfigComplete, validateConfig, NextConfig, + TurboLoaderItem, } from './config-shared' import { loadWebpackHook } from './config-utils' import { ImageConfig, imageConfigDefault } from '../shared/lib/image-config' @@ -852,6 +853,27 @@ export default async function loadConfig( : canonicalBase) || '' } + if ( + userConfig.experimental?.turbo?.loaders && + !userConfig.experimental?.turbo?.rules + ) { + curLog.warn( + 'experimental.turbo.loaders is now deprecated. Please update next.config.js to use experimental.turbo.rules as soon as possible.\n' + + 'The new option is similar, but the key should be a glob instead of an extension.\n' + + 'Example: loaders: { ".mdx": ["mdx-loader"] } -> rules: { "*.mdx": ["mdx-loader"] }" }\n' + + 'See more info here https://nextjs.org/docs/app/api-reference/next-config-js/turbo' + ) + + const rules: Record = {} + for (const [ext, loaders] of Object.entries( + userConfig.experimental.turbo.loaders + )) { + rules['*' + ext] = loaders as TurboLoaderItem[] + } + + userConfig.experimental.turbo.rules = rules + } + onLoadUserConfig?.(userConfig) const completeConfig = assignDefaults( dir,