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
3 changes: 1 addition & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,7 @@ When running Next.js integration tests, you must rebuild if source files have ch

- **First run after branch switch/bootstrap (or if unsure)?**`pnpm build`
- **Edited only core Next.js files (`packages/next/**`) after bootstrap?**`pnpm --filter=next build`
- **Edited Turbopack (Rust)?**`pnpm swc-build-native`
- **Edited both?**`pnpm turbo build build-native`
- **Edited Next.js code or Turbopack (Rust)?**`pnpm build`

## Development Anti-Patterns

Expand Down
110 changes: 110 additions & 0 deletions packages/next-swc/maybe-build-native.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { execSync } from 'child_process'
import { readdirSync, rmSync } from 'fs'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'

const __dirname = dirname(fileURLToPath(import.meta.url))
const PKG_DIR = __dirname
const ROOT_DIR = join(__dirname, '../..')
const NATIVE_DIR = join(PKG_DIR, 'native')

function hasExistingNativeBinary() {
try {
const files = readdirSync(NATIVE_DIR)
return files.some((f) => f.endsWith('.node'))
} catch {
return false
}
}

function clearNativeBinaries() {
try {
const files = readdirSync(NATIVE_DIR)
for (const f of files) {
if (f.endsWith('.node')) {
rmSync(join(NATIVE_DIR, f))
}
}
} catch {
// directory doesn't exist, nothing to clear
}
}

function getVersionBumpCommit() {
try {
return (
execSync(
`git log -1 --format=%H -G '"version":' -- packages/next/package.json`,
{ cwd: ROOT_DIR, encoding: 'utf8' }
).trim() || null
)
} catch {
return null
}
}

function hasRustChanges(sinceCommit) {
try {
// Omit HEAD to compare against the working tree, which includes
// committed, staged, and unstaged changes.
const diff = execSync(
`git diff --name-only ${sinceCommit} -- ':(glob)**/*.rs' ':(glob)**/*.toml' ':(glob).cargo/**' Cargo.lock rust-toolchain`,
{ cwd: ROOT_DIR, encoding: 'utf8' }
).trim()
return diff.length > 0
} catch {
// If we can't determine whether changes occurred, assume they did
return true
}
}

function buildNative() {
console.log('Running swc-build-native...')
execSync('pnpm run swc-build-native', {
cwd: ROOT_DIR,
stdio: 'inherit',
env: {
...process.env,
CARGO_TERM_COLOR: 'always',
TTY: '1',
},
})
}

function main() {
if (process.env.CI) {
console.log('Skipping swc-build-native in CI')
return
}

const versionBumpCommit = getVersionBumpCommit()

if (!versionBumpCommit) {
console.log(
'Could not determine version bump commit (shallow clone?), building native to be safe...'
)
buildNative()
return
}

if (hasRustChanges(versionBumpCommit)) {
console.log(
'Rust source files changed since last version bump, building native...'
)
buildNative()
return
}

// No Rust changes from the release version — clear any stale native build
// so the prebuilt @next/swc-* npm packages are used instead.
if (hasExistingNativeBinary()) {
console.log(
'No Rust changes since last version bump, clearing stale native binary...'
)
clearNativeBinaries()
}

console.log('Skipping swc-build-native (no Rust changes since version bump)')
}

main()
1 change: 1 addition & 0 deletions packages/next-swc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"native/"
],
"scripts": {
"build": "node maybe-build-native.mjs",
"clean": "node ../../scripts/rm.mjs native",
"build-native": "napi build --platform -p next-napi-bindings --cargo-cwd ../../ --cargo-name next_napi_bindings --features plugin,image-extended --js false native",
"build-native-release": "napi build --platform -p next-napi-bindings --cargo-cwd ../../ --cargo-name next_napi_bindings --release --features plugin,image-extended,tracing/release_max_level_trace --js false native",
Expand Down
29 changes: 23 additions & 6 deletions packages/next-swc/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@
"$schema": "https://turborepo.org/schema.json",
"extends": ["//"],
"tasks": {
"build": {
"inputs": [
"../../.cargo/**",
"../../crates/**",
"../../turbopack/crates/**",
"../../Cargo.toml",
"../../Cargo.lock",
"../../.github/workflows/build_and_deploy.yml",
"../../rust-toolchain"
],
"env": ["CI"],
"outputs": [
"native/*.node",
"native/index.d.ts",
"../../packages/next/src/build/swc/generated-native.d.ts"
]
},
"build-native": {
"inputs": [
"../../.cargo/**",
Expand All @@ -13,7 +30,7 @@
"../../rust-toolchain"
],
"env": ["CI"],
"outputs": ["native/*.node"]
"outputs": ["native/*.node", "native/index.d.ts"]
},
"build-native-release": {
"inputs": [
Expand All @@ -26,7 +43,7 @@
"../../rust-toolchain"
],
"env": ["CI"],
"outputs": ["native/*.node"]
"outputs": ["native/*.node", "native/index.d.ts"]
},
"build-native-release-with-assertions": {
"inputs": [
Expand All @@ -39,7 +56,7 @@
"../../rust-toolchain"
],
"env": ["CI"],
"outputs": ["native/*.node"]
"outputs": ["native/*.node", "native/index.d.ts"]
},
"build-native-no-plugin": {
"inputs": [
Expand All @@ -52,7 +69,7 @@
"../../rust-toolchain"
],
"env": ["CI"],
"outputs": ["native/*.node"]
"outputs": ["native/*.node", "native/index.d.ts"]
},
"build-native-no-plugin-release": {
"inputs": [
Expand All @@ -65,7 +82,7 @@
"../../rust-toolchain"
],
"env": ["CI"],
"outputs": ["native/*.node"]
"outputs": ["native/*.node", "native/index.d.ts"]
},
"build-wasm": {
"inputs": [
Expand Down Expand Up @@ -104,7 +121,7 @@
"../../rust-toolchain"
],
"env": ["CI"],
"outputs": ["native/*.node"]
"outputs": ["native/*.node", "native/index.d.ts"]
},
"rust-check": {
"dependsOn": ["rust-check-fmt", "rust-check-clippy", "rust-check-napi"]
Expand Down
61 changes: 46 additions & 15 deletions packages/next/cache.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,53 @@
const cacheExports = {
unstable_cache: require('next/dist/server/web/spec-extension/unstable-cache')
.unstable_cache,
let cacheExports

updateTag: require('next/dist/server/web/spec-extension/revalidate')
.updateTag,
if (process.env.NEXT_RUNTIME === '') {
const notAvailableInClient = (name) => {
return function notAvailable() {
throw new Error(`\`${name}\` is only available in a Server Component.`)
}
}

cacheExports = {
unstable_cache: function unstable_cache(cb) {
// Legacy behavior: allow importing/using unstable_cache from client bundles
// without pulling in server internals.
if (typeof cb !== 'function') return cb
return function cached() {
return cb.apply(this, arguments)
}
},
unstable_noStore: function unstable_noStore() {},

updateTag: notAvailableInClient('updateTag'),
revalidateTag: notAvailableInClient('revalidateTag'),
revalidatePath: notAvailableInClient('revalidatePath'),
refresh: notAvailableInClient('refresh'),
cacheLife: notAvailableInClient('cacheLife'),
cacheTag: notAvailableInClient('cacheTag'),
}
} else {
// Keep server requires in this branch so browser builds can DCE them.
cacheExports = {
unstable_cache:
require('next/dist/server/web/spec-extension/unstable-cache')
.unstable_cache,

updateTag: require('next/dist/server/web/spec-extension/revalidate')
.updateTag,

revalidateTag: require('next/dist/server/web/spec-extension/revalidate')
.revalidateTag,
revalidatePath: require('next/dist/server/web/spec-extension/revalidate')
.revalidatePath,
revalidateTag: require('next/dist/server/web/spec-extension/revalidate')
.revalidateTag,
revalidatePath: require('next/dist/server/web/spec-extension/revalidate')
.revalidatePath,

refresh: require('next/dist/server/web/spec-extension/revalidate').refresh,
refresh: require('next/dist/server/web/spec-extension/revalidate').refresh,

unstable_noStore:
require('next/dist/server/web/spec-extension/unstable-no-store')
.unstable_noStore,
cacheLife: require('next/dist/server/use-cache/cache-life').cacheLife,
cacheTag: require('next/dist/server/use-cache/cache-tag').cacheTag,
unstable_noStore:
require('next/dist/server/web/spec-extension/unstable-no-store')
.unstable_noStore,
cacheLife: require('next/dist/server/use-cache/cache-life').cacheLife,
cacheTag: require('next/dist/server/use-cache/cache-tag').cacheTag,
}
}

let didWarnCacheLife = false
Expand Down
12 changes: 10 additions & 2 deletions packages/next/src/server/stream-utils/uint8array-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ export function indexOfUint8Array(a: Uint8Array, b: Uint8Array) {
if (b.length === 0) return 0
if (a.length === 0 || b.length > a.length) return -1

// Use Node's native implementation when available.
if (typeof Buffer !== 'undefined') {
const haystack = Buffer.isBuffer(a)
? a
: Buffer.from(a.buffer, a.byteOffset, a.byteLength)
return haystack.indexOf(b)
}

// start iterating through `a`
for (let i = 0; i <= a.length - b.length; i++) {
let completeMatch = true
Expand Down Expand Up @@ -50,8 +58,8 @@ export function removeFromUint8Array(a: Uint8Array, b: Uint8Array) {
if (tagIndex === 0) return a.subarray(b.length)
if (tagIndex > -1) {
const removed = new Uint8Array(a.length - b.length)
removed.set(a.slice(0, tagIndex))
removed.set(a.slice(tagIndex + b.length), tagIndex)
removed.set(a.subarray(0, tagIndex))
removed.set(a.subarray(tagIndex + b.length), tagIndex)
return removed
} else {
return a
Expand Down
37 changes: 24 additions & 13 deletions scripts/build-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { promises as fs } from 'node:fs'
import path from 'node:path'
import url from 'node:url'
import execa from 'execa'
import { NEXT_DIR, logCommand, execFn } from './pack-util'
import { NEXT_DIR, logCommand } from './pack-util'

const nextSwcDir = path.join(NEXT_DIR, 'packages/next-swc')

Expand All @@ -25,10 +25,7 @@ export default async function buildNative(
stdio: 'inherit',
})

await execFn(
'Copy generated types to `next/src/build/swc/generated-native.d.ts`',
() => writeTypes()
)
await writeTypes()
}

// Check if this file is being run directly
Expand Down Expand Up @@ -56,17 +53,31 @@ async function writeTypes() {
const generatedTypes = await fs.readFile(generatedTypesPath, 'utf8')
let vendoredTypes = await fs.readFile(vendoredTypesPath, 'utf8')

const existingContent = vendoredTypes
vendoredTypes = vendoredTypes.split(generatedTypesMarker)[0]
vendoredTypes =
vendoredTypes + generatedTypesMarker + generatedNotice + generatedTypes

await fs.writeFile(vendoredTypesPath, vendoredTypes)

const prettifyCommand = ['prettier', '--write', vendoredTypesPath]
const prettifyCommand = ['prettier', '--stdin-filepath', vendoredTypesPath]
logCommand('Prettify generated types', prettifyCommand)
await execa(prettifyCommand[0], prettifyCommand.slice(1), {
cwd: NEXT_DIR,
stdio: 'inherit',
preferLocal: true,
})
const prettierResult = await execa(
prettifyCommand[0],
prettifyCommand.slice(1),
{
cwd: NEXT_DIR,
input: vendoredTypes,
preferLocal: true,
}
)
vendoredTypes = prettierResult.stdout
if (!vendoredTypes.endsWith('\n')) {
vendoredTypes += '\n'
}

if (vendoredTypes === existingContent) {
return
}

logCommand('Write generated types', `write file`)
await fs.writeFile(vendoredTypesPath, vendoredTypes)
}
14 changes: 14 additions & 0 deletions test/production/app-dir/browser-chunks/app/cache-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client'

import { unstable_cache } from 'next/cache'

// Importing next/cache in a Client Component should not pull server internals
// into browser chunks. The bundler sets NEXT_RUNTIME='' for client builds,
// which allows cache.js to DCE the server require() branch.
const getCachedData = unstable_cache(async () => {
return { data: 'hello' }
})

export function CacheClient() {
return <button onClick={() => getCachedData()}>Fetch cached</button>
}
9 changes: 8 additions & 1 deletion test/production/app-dir/browser-chunks/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { CacheClient } from './cache-client'

export default function Page() {
return <p>hello world</p>
return (
<div>
<p>hello world</p>
<CacheClient />
</div>
)
}
Loading
Loading