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
693 changes: 391 additions & 302 deletions Cargo.lock

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -336,25 +336,25 @@ turbopack-trace-utils = { path = "turbopack/crates/turbopack-trace-utils" }
turbopack-wasm = { path = "turbopack/crates/turbopack-wasm" }

# SWC crates
swc_core = { version = "54.0.0", features = [
swc_core = { version = "56.0.0", features = [
"ecma_loader_lru",
"ecma_loader_parking_lot",
"parallel_rayon",
] }
swc_plugin_backend_wasmer = { version = "6.0.0" }
swc_plugin_backend_wasmer = { version = "7.0.0" }
testing = "19.0.0"

# Keep consistent with preset_env_base through swc_core
browserslist-rs = "0.19.0"
mdxjs = "1.0.3"
modularize_imports = "0.108.0"
styled_components = "0.136.0"
styled_jsx = "0.111.0"
swc_emotion = "0.112.0"
swc_relay = "0.82.0"
react_remove_properties = "0.62.0"
remove_console = "0.63.0"
preset_env_base = "6.0.0"
modularize_imports = "0.110.0"
styled_components = "0.138.0"
styled_jsx = "0.113.0"
swc_emotion = "0.114.0"
swc_relay = "0.84.0"
react_remove_properties = "0.64.0"
remove_console = "0.65.0"
preset_env_base = "7.0.0"


# General Deps
Expand Down
22 changes: 14 additions & 8 deletions crates/next-api/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ use turbopack_core::{
asset::AssetContent,
chunk::{
ChunkGroupResult, ChunkingContext, ChunkingContextExt, EvaluatableAsset, EvaluatableAssets,
availability_info::AvailabilityInfo,
SourceMapsType, availability_info::AvailabilityInfo,
},
file_source::FileSource,
ident::{AssetIdent, Layer},
Expand Down Expand Up @@ -1368,13 +1368,19 @@ impl AppEndpoint {
let polyfill_output_asset = ResolvedVc::upcast(polyfill_output);
client_assets.insert(polyfill_output_asset);

let polyfill_source_map_asset = SourceMapAsset::new_fixed(
polyfill_output_path.clone(),
*ResolvedVc::upcast(polyfill_output),
)
.to_resolved()
.await?;
client_assets.insert(ResolvedVc::upcast(polyfill_source_map_asset));
let client_source_maps = project
.next_config()
.client_source_maps(project.next_mode())
.await?;
if *client_source_maps != SourceMapsType::None {
let polyfill_source_map_asset = SourceMapAsset::new_fixed(
polyfill_output_path.clone(),
*ResolvedVc::upcast(polyfill_output),
)
.to_resolved()
.await?;
client_assets.insert(ResolvedVc::upcast(polyfill_source_map_asset));
}

let client_assets: ResolvedVc<OutputAssets> =
ResolvedVc::cell(client_assets.into_iter().collect::<Vec<_>>());
Expand Down
6 changes: 5 additions & 1 deletion crates/next-core/src/next_app/metadata/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,13 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
original_file_content_b64 = StringifyJs(&original_file_content_b64),
};

// Use full filename (stem + extension) to avoid conflicts when multiple icon
// formats exist (e.g., icon.png and icon.svg)
let filename = path.file_name();

let file = File::from(code);
let source = VirtualSource::new(
path.parent().join(&format!("{stem}--route-entry.js"))?,
path.parent().join(&format!("{filename}--route-entry.js"))?,
AssetContent::file(FileContent::Content(file).cell()),
);

Expand Down
14 changes: 5 additions & 9 deletions crates/next-custom-transforms/src/chain_transforms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,21 +161,17 @@ where

fn_pass(move |program| {
if let Some(config) = opts.styled_jsx.to_option() {
let target_browsers = opts
.css_env
.as_ref()
.map(|env| {
targets_to_versions(env.targets.clone(), None)
.expect("failed to parse env.targets")
})
.unwrap_or_default();
let target_browsers = opts.css_env.as_ref().map(|env| {
targets_to_versions(env.targets.clone(), None)
.expect("failed to parse env.targets")
});

program.mutate(styled_jsx::visitor::styled_jsx(
cm.clone(),
&file.name,
&styled_jsx::visitor::Config {
use_lightningcss: config.use_lightningcss,
browsers: *target_browsers,
browsers: *target_browsers.map(|t| t.versions).unwrap_or_default(),
},
&styled_jsx::visitor::NativeConfig { process_css: None },
))
Expand Down
2 changes: 1 addition & 1 deletion crates/next-custom-transforms/tests/full.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fn test(input: &Path, minify: bool) {
output_path: Some(output.clone()),

config: swc_core::base::config::Config {
is_module: Some(swc_core::base::config::IsModule::Bool(true)),
is_module: Some(swc_core::base::config::IsModule::Unknown),

jsc: swc_core::base::config::JscConfig {
minify: if minify {
Expand Down
11 changes: 3 additions & 8 deletions crates/next-custom-transforms/tests/loader/auto-cjs/1/output.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _interop_require_default = require("@swc/helpers/_/_interop_require_default");
var _esm = /*#__PURE__*/ _interop_require_default._(require("esm"));
console.log(_esm.default.foo);
module.exports = _esm.default;
import mixed from 'esm';
console.log(mixed.foo);
module.exports = mixed;
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _class_call_check = require("@swc/helpers/_/_class_call_check");
// This file should not import helpers using ESM syntax
var E = function Foo() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import _JSXStyle from "styled-jsx/style";
export default function Foo() {
return /*#__PURE__*/ React.createElement("div", {
render: function(v) {
render: function render(v) {
return /*#__PURE__*/ React.createElement("form", {
className: "jsx-3d44fb7892a1f38b"
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
;
/*#__PURE__*/ React.createElement("div", null, "children");
'<>hello</>';
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
"use strict";
function loadSomethingWithDynamicImport(param) {
return import("something/".concat(param)).then(function(r) {
return r.default;
Expand Down
1 change: 1 addition & 0 deletions crates/next-napi-bindings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ once_cell = { workspace = true }

swc_core = { workspace = true, features = [
"base",
"base_module",
"common_concurrent",
"ecma_ast",
"ecma_ast_serde",
Expand Down
11 changes: 9 additions & 2 deletions docs/01-app/03-api-reference/04-functions/cacheLife.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ Cache profiles control caching behavior through three timing properties:

During this time, the client-side router displays cached content immediately without any network request. After this period expires, the router must check with the server on the next navigation or request. This provides instant page loads from the client cache, but data may be outdated.

- If omitted, defaults to the `default` profile's `stale` value (5 minutes, see [`staleTimes`](/docs/app/api-reference/config/next-config-js/staleTimes))

```tsx
cacheLife({ stale: 300 }) // 5 minutes
```
Expand All @@ -106,6 +108,7 @@ How often the server regenerates cached content in the background.
2. Regenerates content in the background
3. Updates the cache with fresh content
- Similar to [Incremental Static Regeneration (ISR)](/docs/app/guides/incremental-static-regeneration)
- If omitted, defaults to the `default` profile's `revalidate` value (15 minutes)

```tsx
cacheLife({ revalidate: 900 }) // 15 minutes
Expand All @@ -117,6 +120,7 @@ Maximum time before the server must regenerate cached content.

- After this period with no traffic, the server regenerates content synchronously on the next request
- When you set both `revalidate` and `expire`, `expire` must be longer than `revalidate`. Next.js validates this and raises an error for invalid configurations.
- If omitted, defaults to the `default` profile's `expire` value (never expires)

```tsx
cacheLife({ expire: 3600 }) // 1 hour
Expand All @@ -128,7 +132,7 @@ If you don't specify a profile, Next.js uses the `default` profile. We recommend

| **Profile** | **Use Case** | `stale` | `revalidate` | `expire` |
| ----------- | -------------------------------------- | ---------- | ------------ | -------- |
| `default` | Standard content | 5 minutes | 15 minutes | 1 year |
| `default` | Standard content | 5 minutes | 15 minutes | never |
| `seconds` | Real-time data | 30 seconds | 1 second | 1 minute |
| `minutes` | Frequently updated content | 5 minutes | 1 minute | 1 hour |
| `hours` | Content updated multiple times per day | 5 minutes | 1 hour | 1 day |
Expand Down Expand Up @@ -174,6 +178,8 @@ module.exports = nextConfig

The example above caches for 14 days, checks for updates daily, and expires the cache after 14 days. You can then reference this profile throughout your application by its name:

> **Good to know**: Any omitted properties in a custom profile inherit from the `default` profile. This also applies to inline profile objects passed directly to `cacheLife()`.

```tsx filename="app/page.tsx" highlight={5}
'use cache'
import { cacheLife } from 'next/cache'
Expand Down Expand Up @@ -552,7 +558,7 @@ This pattern is useful when different outcomes need different cache durations, f

If you want to calculate cache lifetime at runtime, for example by reading it from the fetched data, use an [inline cache profile](#inline-cache-profiles) object:

```tsx filename="lib/posts.ts" highlight={15,16,17}
```tsx filename="lib/posts.ts" highlight={15,16,17,18}
import { cacheLife, cacheTag } from 'next/cache'

async function getPostContent(slug: string) {
Expand All @@ -569,6 +575,7 @@ async function getPostContent(slug: string) {
// Use cache timing from CMS data directly as an object
cacheLife({
// Ensure post.revalidateSeconds is a number in seconds
// stale and expire inherit from 'default' profile
revalidate: post.revalidateSeconds ?? 3600,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,34 @@ module.exports = nextConfig

## Trade-Offs

### When to Use Inline CSS
- **Enable** if you use atomic CSS (like Tailwind) and want to optimize first-load performance for new visitors
- **Skip** if returning visitors are common and you want them to benefit from cached stylesheets

Inlining CSS can be beneficial in several scenarios:
### When Inline CSS Helps

- **First-Time Visitors**: Since CSS files are render-blocking resources, inlining eliminates the initial download delay that first-time visitors experience, improving page load performance.
Normally, the browser must download HTML, parse it, discover CSS `<link>` tags, then request stylesheets before it can render. Inlining [eliminates this request waterfall](https://web.dev/learn/performance/optimize-resource-loading#inline_critical_css), so that styles arrive with the HTML, so the browser can render immediately.

- **Performance Metrics**: By removing the additional network requests for CSS files, inlining can significantly improve key metrics like First Contentful Paint (FCP) and Largest Contentful Paint (LCP).
This benefit is strongest with:

- **Slow Connections**: For users on slower networks where each request adds considerable latency, inlining CSS can provide a noticeable performance boost by reducing network roundtrips.
- **First-time visitors**: Since CSS files are render-blocking, inlining eliminates the initial download delay that first-time visitors experience. Returning visitors with cached stylesheets won't see this benefit.

- **Atomic CSS Bundles (e.g., Tailwind)**: With utility-first frameworks like Tailwind CSS, the size of the styles required for a page is often O(1) relative to the complexity of the design. This makes inlining a compelling choice because the entire set of styles for the current page is lightweight and doesn’t grow with the page size. Inlining Tailwind styles ensures minimal payload and eliminates the need for additional network requests, which can further enhance performance.
- **Performance metrics**: By removing additional network requests for CSS files, inlining can significantly improve [First Contentful Paint (FCP)](https://web.dev/articles/fcp) and [Largest Contentful Paint (LCP)](https://web.dev/articles/lcp).

### When Not to Use Inline CSS
- **Slow connections**: For users on high-latency networks, each additional request adds delay. Inlining reduces round trips, which matters most when connections are slow.

While inlining CSS offers significant benefits for performance, there are scenarios where it may not be the best choice:
- **Atomic CSS (Tailwind)**: Utility-first frameworks generate only the classes you use, keeping CSS small. The styles for a page don't grow proportionally with page complexity—they're typically compact regardless of how much UI you build. This makes inlining practical since you get the performance benefit without significantly bloating HTML.

- **Large CSS Bundles**: If your CSS bundle is too large, inlining it may significantly increase the size of the HTML, resulting in slower Time to First Byte (TTFB) and potentially worse performance for users with slow connections.
- **Dynamic or Page-Specific CSS**: For applications with highly dynamic styles or pages that use different sets of CSS, inlining may lead to redundancy and bloat, as the full CSS for all pages may need to be inlined repeatedly.
### When External CSS is Better

- **Browser Caching**: In cases where visitors frequently return to your site, external CSS files allow browsers to cache styles efficiently, reducing data transfer for subsequent visits. Inlining CSS eliminates this benefit.
Inlined styles cannot be cached separately from HTML. Every page load re-downloads the same CSS.

Evaluate these trade-offs carefully, and consider combining inlining with other strategies, such as critical CSS extraction or a hybrid approach, for the best results tailored to your site's needs.
This trade-off matters most with:

- **Returning visitors**: Users who visit your site repeatedly would benefit from cached external stylesheets. With inlining, they re-download styles on every visit.

- **Large CSS bundles**: External stylesheets cache independently and load efficiently on modern infrastructure. Inlined CSS arrives with every HTML response, increasing [Time to First Byte (TTFB)](https://web.dev/articles/ttfb) and preventing browsers from caching styles separately. This trade-off works for small CSS (atomic frameworks like Tailwind), but adds overhead for larger bundles (component libraries like Bootstrap or Material UI).

- **Many pages sharing styles**: External stylesheets cached on one page speed up navigation to other pages. Inlined styles provide no cross-page caching benefit.

> **Good to know**:
>
Expand Down
57 changes: 53 additions & 4 deletions errors/next-image-unconfigured-localpatterns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,76 @@ title: '`next/image` Un-configured localPatterns'

## Why This Error Occurred

One of your pages that leverages the `next/image` component, passed a `src` value that uses a URL that isn't defined in the `images.localPatterns` property in `next.config.js`.
One of your pages that leverages the `next/image` component, passed a `src` value that uses a local path that isn't allowed by the `images.localPatterns` configuration in `next.config.js`.

Each part of the `src` value is matched against your `images.localPatterns` definitions:

- **Pathname**: The path must be covered by your glob pattern, e.g. `/**` or `/assets/**`. Single `*` matches a single path segment, while double `**` matches any number of path segments.
- **Search**: If specified in a pattern, it must match the full search string exactly (including the leading `?`). Globs are not supported for search.

If any of these differ from the actual `src`, the image will be rejected.

Common pitfalls that cause this error:

- A too-narrow pathname pattern (e.g. `/assets/` instead of `/assets/**`).
- Setting `search: ''` when your images include query strings like `?v=123` or `?t=timestamp`. An empty string means only URLs **without** query strings are allowed.
- Forgetting that pathname patterns are case-sensitive and must match exactly.

See the [Local Patterns](/docs/pages/api-reference/components/image#localpatterns) reference for details.

## Possible Ways to Fix It

Add an entry to `images.localPatterns` array in `next.config.js` with the expected URL pattern. For example:
Add the pathname to the `images.localPatterns` config in `next.config.js`:

```js filename="next.config.js"
module.exports = {
images: {
localPatterns: [
{
pathname: '/assets/**',
},
],
},
}
```

### Any search params (default)

To allow any search params, omit the `search` key:

```js filename="next.config.js"
module.exports = {
images: {
localPatterns: [
{
pathname: '/api/images/**',
// search is omitted, so ?v=123, ?t=456, or no query string are all allowed
},
],
},
}
```

### With specific search params

To allow only a specific search param value:

```js filename="next.config.js"
module.exports = {
images: {
localPatterns: [
{
pathname: '/assets/**',
search: '?v=1',
},
],
},
}
```

Omitting the `search` will allow all query strings.
### No search params

If you want to prevent the query string from matching, you can use the empty string, for example:
To disallow query strings entirely, use an empty string:

```js filename="next.config.js"
module.exports = {
Expand Down
4 changes: 1 addition & 3 deletions packages/next/src/build/webpack/loaders/metadata/discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ async function enumMetadataFiles(
)
for (const name of possibleFileNames) {
const resolved = await metadataResolver(dir, name, extensions)
if (resolved) {
collectedFiles.push(resolved)
}
collectedFiles.push(...resolved)
}

return collectedFiles
Expand Down
Loading
Loading