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
9 changes: 9 additions & 0 deletions docs/01-app/03-api-reference/08-turbopack.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,15 @@ module.exports = {
}
```

### CSS / Sass / SCSS decimal precision

Turbopack uses [Lightning CSS](https://lightningcss.dev/) to compile CSS. Lightning CSS uses 5 digits of decimal precision for numeric CSS values, while webpack uses 10 digits. This applies to both plain CSS and Sass/SCSS output. For example, a value that evaluates to `25/17` produces:

- **Webpack:** `line-height: 1.4705882353` (10 digits)
- **Turbopack:** `line-height: 1.47059` (5 digits)

This can lead to subtle rendering differences when migrating from webpack to Turbopack, especially for properties like `line-height`, `letter-spacing`, or other calculated values where high precision matters.

### Build Caching

Webpack supports [disk build caching](https://webpack.js.org/configuration/cache/#cache) to improve build performance. Turbopack provides a similar feature, currently in beta. Starting with Next 16, you can enable Turbopack’s filesystem cache by setting the following experimental flags:
Expand Down
99 changes: 91 additions & 8 deletions packages/create-next-app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,19 +251,45 @@ async function run(): Promise<void> {
type DisplayConfigItem = {
key: keyof typeof defaults
values?: Record<string, string>
flags?: Record<string, string>
}

const displayConfig: DisplayConfigItem[] = [
{
key: 'typescript',
values: { true: 'TypeScript', false: 'JavaScript' },
flags: { true: '--ts', false: '--js' },
},
{
key: 'linter',
values: { eslint: 'ESLint', biome: 'Biome', none: 'None' },
flags: { eslint: '--eslint', biome: '--biome', none: '--no-eslint' },
},
{
key: 'reactCompiler',
values: { true: 'React Compiler', false: 'No React Compiler' },
flags: { true: '--react-compiler', false: '--no-react-compiler' },
},
{
key: 'tailwind',
values: { true: 'Tailwind CSS', false: 'No Tailwind CSS' },
flags: { true: '--tailwind', false: '--no-tailwind' },
},
{
key: 'srcDir',
values: { true: 'src/ directory', false: 'No src/ directory' },
flags: { true: '--src-dir', false: '--no-src-dir' },
},
{
key: 'app',
values: { true: 'App Router', false: 'Pages Router' },
flags: { true: '--app', false: '--no-app' },
},
{
key: 'agentsMd',
values: { true: 'AGENTS.md', false: 'No AGENTS.md' },
flags: { true: '--agents-md', false: '--no-agents-md' },
},
{ key: 'linter', values: { eslint: 'ESLint', biome: 'Biome' } },
{ key: 'reactCompiler', values: { true: 'React Compiler' } },
{ key: 'tailwind', values: { true: 'Tailwind CSS' } },
{ key: 'srcDir', values: { true: 'src/ dir' } },
{ key: 'app', values: { true: 'App Router', false: 'Pages Router' } },
{ key: 'agentsMd', values: { true: 'AGENTS.md' } },
]

// Helper to format settings for display based on displayConfig
Expand Down Expand Up @@ -291,10 +317,17 @@ async function run(): Promise<void> {
const hasSavedPreferences = Object.keys(preferences).length > 0

// Check if user provided any configuration flags
// If they did, skip the "recommended defaults" prompt and go straight to
// individual prompts for any missing options
// If they did, skip all prompts and use recommended defaults for unspecified
// options. This is critical for AI agents, which pass flags like
// --typescript --tailwind --app and expect the rest to use sensible defaults
// without entering interactive mode.
const hasProvidedOptions = process.argv.some((arg) => arg.startsWith('--'))

if (!skipPrompt && hasProvidedOptions) {
skipPrompt = true
useRecommendedDefaults = true
}

// Only show the "recommended defaults" prompt if:
// - Not in CI and not using --yes flag
// - User hasn't provided any custom options
Expand Down Expand Up @@ -620,6 +653,56 @@ async function run(): Promise<void> {
preferences.agentsMd = Boolean(agentsMd)
}
}

// When prompts were skipped because flags were provided, print the
// defaults that were assumed so agents and users know what to override.
if (hasProvidedOptions && useRecommendedDefaults) {
const lines: string[] = []

for (const config of displayConfig) {
if (!config.flags || !config.values) continue

// Skip options the user already specified explicitly
const wasExplicit = process.argv.some((arg) =>
Object.values(config.flags!).includes(arg)
)
if (wasExplicit) continue

const value = String(defaults[config.key])
const flag = config.flags[value]
const label = config.values[value]
if (!flag || !label) continue

// Show alternatives the user could pass instead
const alts: string[] = []
for (const [k, f] of Object.entries(config.flags)) {
if (k !== value && config.values[k]) {
alts.push(`${f} for ${config.values[k]}`)
}
}

const altText = alts.length > 0 ? ` (use ${alts.join(', ')})` : ''
lines.push(` ${flag.padEnd(24)}${label}${altText}`)
}

// Import alias is not a boolean toggle, handle separately
const hasImportAlias = process.argv.some(
(arg) =>
arg.startsWith('--import-alias') ||
arg.startsWith('--no-import-alias')
)
if (!hasImportAlias) {
lines.push(` ${'--import-alias'.padEnd(24)}"${defaults.importAlias}"`)
}

if (lines.length > 0) {
console.log(
'\nUsing defaults for unprovided options:\n\n' +
lines.join('\n') +
'\n'
)
}
}
}

const bundler: Bundler = opts.rspack ? Bundler.Rspack : Bundler.Turbopack
Expand Down
19 changes: 8 additions & 11 deletions packages/next/src/build/swc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,16 +229,6 @@ export async function loadBindings(
}

pendingBindings = new Promise(async (resolve, reject) => {
if (!lockfilePatchPromise.cur) {
// always run lockfile check once so that it gets patched
// even if it doesn't fail to load locally
lockfilePatchPromise.cur = (
require('../../lib/patch-incorrect-lockfile') as typeof import('../../lib/patch-incorrect-lockfile')
)
.patchIncorrectLockfile(process.cwd())
.catch(console.error)
}

let attempts: any[] = []
const disableWasmFallback = process.env.NEXT_DISABLE_SWC_WASM
const unsupportedPlatform = triples.some(
Expand Down Expand Up @@ -428,7 +418,14 @@ async function logLoadFailure(attempts: any, triedWasm = false) {
wasm: triedWasm ? 'failed' : undefined,
nativeBindingsErrorCode: lastNativeBindingsLoadErrorCode,
})
await (lockfilePatchPromise.cur || Promise.resolve())
if (!lockfilePatchPromise.cur) {
lockfilePatchPromise.cur = (
require('../../lib/patch-incorrect-lockfile') as typeof import('../../lib/patch-incorrect-lockfile')
)
.patchIncorrectLockfile(process.cwd())
.catch(console.error)
}
await lockfilePatchPromise.cur

Log.error(
`Failed to load SWC binary for ${PlatformName}/${ArchName}, see more info here: https://nextjs.org/docs/messages/failed-loading-swc`
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/client/normalize-trailing-slash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { parsePath } from '../shared/lib/router/utils/parse-path'
* in `next.config.js`.
*/
export const normalizePathTrailingSlash = (path: string) => {
if (!path.startsWith('/') || process.env.__NEXT_MANUAL_TRAILING_SLASH) {
// 47 is the char code for '/'
if (path.charCodeAt(0) !== 47 || process.env.__NEXT_MANUAL_TRAILING_SLASH) {
return path
}

Expand Down
6 changes: 3 additions & 3 deletions packages/next/src/lib/patch-incorrect-lockfile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { promises } from 'fs'
import { readFileSync, writeFileSync } from 'fs'
import * as Log from '../build/output/log'
import findUp from 'next/dist/compiled/find-up'
// @ts-ignore no-json types
Expand Down Expand Up @@ -46,7 +46,7 @@ export async function patchIncorrectLockfile(dir: string) {
// if no lockfile present there is no action to take
return
}
const content = await promises.readFile(lockfilePath, 'utf8')
const content = readFileSync(lockfilePath, 'utf8')
// maintain current line ending
const endingNewline = content.endsWith('\r\n')
? '\r\n'
Expand Down Expand Up @@ -161,7 +161,7 @@ export async function patchIncorrectLockfile(dir: string) {
}
}

await promises.writeFile(
writeFileSync(
lockfilePath,
JSON.stringify(lockfileParsed, null, 2) + endingNewline
)
Expand Down
22 changes: 18 additions & 4 deletions packages/next/src/server/api-utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IncomingMessage } from 'http'
import type { IncomingHttpHeaders, IncomingMessage } from 'http'
import type { BaseNextRequest } from '../base-http'
import type { CookieSerializeOptions } from 'next/dist/compiled/cookie'
import type { NextApiResponse } from '../../shared/lib/utils'
Expand Down Expand Up @@ -83,12 +83,26 @@ export function checkIsOnDemandRevalidate(
isOnDemandRevalidate: boolean
revalidateOnlyGenerated: boolean
} {
const headers = HeadersAdapter.from(req.headers)
// Headers is a plain object for Node.js, Headers object in Edge runtime
if (typeof req.headers.get === 'function') {
const headers = HeadersAdapter.from(req.headers)

const previewModeId = headers.get(PRERENDER_REVALIDATE_HEADER)
const previewModeId = headers.get(PRERENDER_REVALIDATE_HEADER)
const isOnDemandRevalidate = previewModeId === previewProps.previewModeId

const revalidateOnlyGenerated = headers.has(
PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER
)

return { isOnDemandRevalidate, revalidateOnlyGenerated }
}

const headers = req.headers as IncomingHttpHeaders

const previewModeId = headers[PRERENDER_REVALIDATE_HEADER]
const isOnDemandRevalidate = previewModeId === previewProps.previewModeId

const revalidateOnlyGenerated = headers.has(
const revalidateOnlyGenerated = headers.hasOwnProperty(
PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER
)

Expand Down
Loading
Loading