Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use turbo_tasks_env::EnvMap;
use turbo_tasks_fetch::FetchClientConfig;
use turbo_tasks_fs::FileSystemPath;
use turbopack::module_options::{
ConditionItem, ConditionPath, ConditionQuery, LoaderRuleItem, WebpackRules,
module_options_context::MdxTransformOptions,
ConditionContentType, ConditionItem, ConditionPath, ConditionQuery, LoaderRuleItem,
WebpackRules, module_options_context::MdxTransformOptions,
};
use turbopack_core::{
chunk::SourceMapsType,
Expand Down Expand Up @@ -709,6 +709,42 @@ impl TryFrom<ConfigConditionQuery> for ConditionQuery {
}
}

#[derive(
Clone,
PartialEq,
Eq,
Debug,
Deserialize,
TraceRawVcs,
NonLocalValue,
OperationValue,
Encode,
Decode,
)]
#[serde(
tag = "type",
content = "value",
rename_all = "camelCase",
deny_unknown_fields
)]
pub enum ConfigConditionContentType {
Glob(RcStr),
Regex(RegexComponents),
}

impl TryFrom<ConfigConditionContentType> for ConditionContentType {
type Error = anyhow::Error;

fn try_from(config: ConfigConditionContentType) -> Result<ConditionContentType> {
Ok(match config {
ConfigConditionContentType::Glob(value) => ConditionContentType::Glob(value),
ConfigConditionContentType::Regex(regex) => {
ConditionContentType::Regex(EsRegex::try_from(regex)?.resolved_cell())
}
})
}
}

#[derive(
Deserialize,
Clone,
Expand Down Expand Up @@ -741,6 +777,8 @@ pub enum ConfigConditionItem {
content: Option<RegexComponents>,
#[serde(default)]
query: Option<ConfigConditionQuery>,
#[serde(default, rename = "contentType")]
content_type: Option<ConfigConditionContentType>,
},
}

Expand All @@ -765,13 +803,17 @@ impl TryFrom<ConfigConditionItem> for ConditionItem {
path,
content,
query,
content_type,
} => ConditionItem::Base {
path: path.map(ConditionPath::try_from).transpose()?,
content: content
.map(EsRegex::try_from)
.transpose()?
.map(EsRegex::resolved_cell),
query: query.map(ConditionQuery::try_from).transpose()?,
content_type: content_type
.map(ConditionContentType::try_from)
.transpose()?,
},
})
}
Expand Down Expand Up @@ -2151,6 +2193,7 @@ mod tests {
source: rcstr!("@someQuery"),
flags: rcstr!(""),
})),
content_type: None,
},
]
.into(),
Expand Down
2 changes: 2 additions & 0 deletions crates/next-core/src/next_shared/webpack_rules/babel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ pub async fn get_babel_loader_rules(
.resolved_cell(),
),
query: None,
content_type: None,
});
}
ReactCompilerCompilationMode::Infer => {
Expand All @@ -212,6 +213,7 @@ pub async fn get_babel_loader_rules(
.resolved_cell(),
),
query: None,
content_type: None,
});
}
ReactCompilerCompilationMode::All => {}
Expand Down
4 changes: 2 additions & 2 deletions docs/01-app/01-getting-started/10-error-handling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ Create an error boundary by adding an [`error.js`](/docs/app/api-reference/file-

import { useEffect } from 'react'

export default function Error({
export default function ErrorPage({
error,
reset,
}: {
Expand Down Expand Up @@ -239,7 +239,7 @@ export default function Error({

import { useEffect } from 'react'

export default function Error({ error, reset }) {
export default function ErrorPage({ error, reset }) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,10 +216,11 @@ module.exports = {
```

- Supported boolean operators are `{all: [...]}`, `{any: [...]}` and `{not: ...}`.
- Supported customizable operators are `{path: string | RegExp}`, `{content: RegExp}`, and `{query: string | RegExp}`. If multiple operators are specified in the same object, it acts as an implicit `and`.
- `path` matches against the project-relative file path. A string is treated as a glob pattern, while a RegExp matches anywhere in the path.
- Supported customizable operators are `{path: string | RegExp}`, `{content: RegExp}`, `{query: string | RegExp}`, and `{contentType: string | RegExp}`. If multiple operators are specified in the same object, it acts as an implicit `and`.
- `path` matches against the project-relative file path. A string is treated as a glob pattern, while a RegExp can be used to match the path partially.
- `content` matches anywhere in the file content.
- `query` matches the import's query string (e.g., `?foo` in `import './file?foo'`). A string must match exactly, while a RegExp can match partially.
- `query` matches the import's query string (e.g., `?foo` in `import './file?foo'`). A string must match exactly, while a RegExp can be used to match the query string partially.
- `contentType` matches the MIME content type of the resource (e.g., from data URLs like `data:text/plain,...`). A string is treated as a glob pattern (e.g., `text/*`, `image/*`), while a RegExp can be used to match the content type partially.

In addition, a number of built-in conditions are supported:

Expand Down Expand Up @@ -312,10 +313,11 @@ The option automatically adds a polyfill for debug IDs to the JavaScript bundle

## Version History

| Version | Changes |
| -------- | ----------------------------------------------- |
| `16.2.0` | `turbopack.rules.*.condition.query` was added. |
| `16.0.0` | `turbopack.debugIds` was added. |
| `16.0.0` | `turbopack.rules.*.condition` was added. |
| `15.3.0` | `experimental.turbo` is changed to `turbopack`. |
| `13.0.0` | `experimental.turbo` introduced. |
| Version | Changes |
| -------- | ---------------------------------------------------- |
| `16.2.0` | `turbopack.rules.*.condition.contentType` was added. |
| `16.2.0` | `turbopack.rules.*.condition.query` was added. |
| `16.0.0` | `turbopack.debugIds` was added. |
| `16.0.0` | `turbopack.rules.*.condition` was added. |
| `15.3.0` | `experimental.turbo` is changed to `turbopack`. |
| `13.0.0` | `experimental.turbo` introduced. |
2 changes: 1 addition & 1 deletion packages/font/src/google/font-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -7551,7 +7551,7 @@
"Kedebideri": {
"weights": ["400", "500", "600", "700", "800", "900"],
"styles": ["normal"],
"subsets": ["latin"]
"subsets": ["beria-erfe", "latin"]
},
"Kelly Slab": {
"weights": ["400"],
Expand Down
2 changes: 1 addition & 1 deletion packages/font/src/google/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11920,7 +11920,7 @@ export declare function Kedebideri<
preload?: boolean
fallback?: string[]
adjustFontFallback?: boolean
subsets?: Array<'latin'>
subsets?: Array<'beria-erfe' | 'latin'>
}): T extends undefined ? NextFont : NextFontWithVariable
export declare function Kelly_Slab<
T extends CssVariable | undefined = undefined,
Expand Down
12 changes: 12 additions & 0 deletions packages/next/src/build/swc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,9 @@ function bindingToApi(
query?:
| { type: 'regex'; value: { source: string; flags: string } }
| { type: 'constant'; value: string }
contentType?:
| { type: 'regex'; value: { source: string; flags: string } }
| { type: 'glob'; value: string }
}

// converts regexes to a `RegexComponents` object so that it can be JSON-serialized when passed to
Expand Down Expand Up @@ -997,6 +1000,15 @@ function bindingToApi(
value: regexComponents(cond.query),
}
: { type: 'constant', value: cond.query },
contentType:
cond.contentType == null
? undefined
: cond.contentType instanceof RegExp
? {
type: 'regex',
value: regexComponents(cond.contentType),
}
: { type: 'glob', value: cond.contentType },
}
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ const zTurbopackCondition: zod.ZodType<TurbopackRuleCondition> = z.union([
path: z.union([z.string(), z.instanceof(RegExp)]).optional(),
content: z.instanceof(RegExp).optional(),
query: z.union([z.string(), z.instanceof(RegExp)]).optional(),
contentType: z.union([z.string(), z.instanceof(RegExp)]).optional(),
}),
])

Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export type TurbopackRuleCondition =
path?: string | RegExp
content?: RegExp
query?: string | RegExp
contentType?: string | RegExp
}

export type TurbopackRuleConfigItem = {
Expand Down
3 changes: 1 addition & 2 deletions packages/next/src/shared/lib/router/utils/querystring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ function stringifyUrlQueryParam(param: unknown): string {

if (
(typeof param === 'number' && !isNaN(param)) ||
typeof param === 'boolean' ||
typeof param === 'bigint'
typeof param === 'boolean'
) {
return String(param)
} else {
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/app-dir/turbopack-loader-content-type/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
16 changes: 16 additions & 0 deletions test/e2e/app-dir/turbopack-loader-content-type/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// @ts-expect-error -- data URL import
import textData from 'data:text/plain,Hello World'
// @ts-expect-error -- data URL import
import jsData from 'data:text/javascript,export default "Hello JS"'
// @ts-expect-error -- data URL import
import imageData from 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='

export default function Page() {
return (
<div>
<p id="text">{textData}</p>
<p id="js">{jsData}</p>
<p id="image">{imageData}</p>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function imageLoader(source) {
return `export default ${JSON.stringify('IMAGE:' + source.length + ' bytes')}`
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/turbopack-loader-content-type/js-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function jsLoader(source) {
return source.replace('Hello JS', 'Hello from loader')
}
52 changes: 52 additions & 0 deletions test/e2e/app-dir/turbopack-loader-content-type/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
turbopack: {
rules: {
'*': [
{
// Exact match for text/javascript content type
condition: { contentType: 'text/javascript' },
loaders: [require.resolve('./js-loader.js')],
as: '*.js',
},
{
// Glob pattern match for text content types
// Should not be applied to text/javascript due to the order of rules
condition: { contentType: 'text/*' },
loaders: [require.resolve('./text-loader.js')],
as: '*.js',
},
{
// Regex match for image content types
condition: { contentType: /^image\// },
loaders: [require.resolve('./image-loader.js')],
as: '*.js',
},
],
},
},
webpack: (config) => {
config.module.rules.push(
{
mimetype: 'text/javascript',
use: [{ loader: require.resolve('./js-loader.js') }],
type: 'javascript/auto',
},
{
mimetype: /^text\/(?!javascript$)/,
use: [{ loader: require.resolve('./text-loader.js') }],
type: 'javascript/auto',
},
{
mimetype: /^image\//,
use: [{ loader: require.resolve('./image-loader.js') }],
type: 'javascript/auto',
}
)
return config
},
}

module.exports = nextConfig
3 changes: 3 additions & 0 deletions test/e2e/app-dir/turbopack-loader-content-type/text-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function textLoader(source) {
return `export default ${JSON.stringify('TEXT:' + source)}`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { nextTestSetup } from 'e2e-utils'

describe('turbopack-loader-content-type', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
})

if (skipped) return

it('should apply loader based on contentType glob pattern', async () => {
const $ = await next.render$('/')
const text = $('#text').text()
expect(text).toBe('TEXT:Hello World')
})

it('should apply loader based on contentType for text/javascript', async () => {
const $ = await next.render$('/')
const text = $('#js').text()
expect(text).toBe('Hello from loader')
})

it('should apply loader based on contentType regex', async () => {
const $ = await next.render$('/')
const text = $('#image').text()
expect(text).toMatch(/^IMAGE:\d+ bytes$/)
})
})
1 change: 1 addition & 0 deletions turbopack/crates/turbopack-core/src/ident.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ impl AssetIdent {
let root = self.path.root().await?;
let path = self.path.clone();
self.path = root.join(&pattern.replace('*', &path.path))?;
self.content_type = None;
Ok(())
}
}
Expand Down
12 changes: 12 additions & 0 deletions turbopack/crates/turbopack/src/module_options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ async fn rule_condition_from_webpack_condition(
path,
content,
query,
content_type,
} => {
let mut rule_conditions = Vec::new();
match &path {
Expand All @@ -141,6 +142,17 @@ async fn rule_condition_from_webpack_condition(
}
None => {}
}
match &content_type {
Some(ConditionContentType::Glob(glob)) => {
rule_conditions.push(RuleCondition::ContentTypeGlob(
Glob::new(glob.clone(), GlobOptions::default()).await?,
));
}
Some(ConditionContentType::Regex(regex)) => {
rule_conditions.push(RuleCondition::ContentTypeEsRegex(regex.await?));
}
None => {}
}
// Add the content condition last since matching requires a more expensive file read.
if let Some(content) = content {
rule_conditions.push(RuleCondition::ResourceContentEsRegex(content.await?));
Expand Down
Loading
Loading