first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

1
scripts/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
pr-status/*

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<!-- Periodically compress text-heavy directories -->
<dict>
<key>Label</key>
<string>build.turbo.compress</string>
<key>StandardOutPath</key>
<string>/tmp/build.turbo.compress/log</string>
<key>StandardErrorPath</key>
<string>/tmp/build/turbo.compress/error</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/opt/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>cd $(defaults read build.turbo.repopath -string) &amp;&amp; ./scripts/macos-compress.sh</string>
</array>
<key>StartInterval</key>
<integer>10800</integer> <!-- 3 hours -->
</dict>
</plist>

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
#
# Optionally automates compressing target/ and other directories.
# Only intended for and needed on macOS.
set -e
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
NEXT_DIR=$(realpath "$SCRIPT_DIR/../..")
defaults write build.turbo.repopath -string "$NEXT_DIR"
for filename in "$SCRIPT_DIR"/*.plist; do
PLIST_FILE="$HOME/Library/LaunchAgents/$(basename "$filename")"
ln -sf "$filename" "$PLIST_FILE"
launchctl unload "$PLIST_FILE" || true
launchctl load "$PLIST_FILE"
done

View File

@@ -0,0 +1,142 @@
#!/usr/bin/env node
/**
* Dev Server Bundle Analyzer
*
* Generates a bundle analyzer report for the dev server bundle.
*
* Usage:
* node scripts/analyze-bundle.js [options]
*
* Options:
* --open Open the report in browser (default: false)
* --verbose Show detailed module reasons
* --json Also output stats.json file
* --list-modules List all bundled modules to console
* --list-externals List all externalized modules
*/
const { execSync } = require('child_process')
const path = require('path')
const fs = require('fs')
// Parse arguments
const args = process.argv.slice(2)
const hasFlag = (name) => args.includes(`--${name}`)
const openBrowser = hasFlag('open')
const verbose = hasFlag('verbose')
const outputJson = hasFlag('json')
const listModules = hasFlag('list-modules')
const listExternals = hasFlag('list-externals')
const nextDir = path.join(__dirname, '..', 'packages', 'next')
const bundlePath = path.join(
nextDir,
'dist/compiled/dev-server/start-server.js'
)
const reportPath = path.join(
nextDir,
'dist/compiled/dev-server/bundle-report.html'
)
console.log('\x1b[34m=== Dev Server Bundle Analyzer ===\x1b[0m')
console.log('')
// Build with analyzer
console.log('Building bundle with analyzer...')
const env = {
...process.env,
ANALYZE: '1',
...(verbose ? { ANALYZE_REASONS: '1' } : {}),
}
try {
execSync('npx taskr next_bundle_dev_server', {
cwd: nextDir,
stdio: verbose ? 'inherit' : 'pipe',
env,
})
} catch (err) {
console.error('\x1b[31mBuild failed\x1b[0m')
process.exit(1)
}
// Get bundle stats
const stats = fs.statSync(bundlePath)
const sizeKB = Math.round(stats.size / 1024)
const sizeMB = (stats.size / (1024 * 1024)).toFixed(2)
console.log('')
console.log('\x1b[32mBundle Stats:\x1b[0m')
console.log(` Size: ${sizeKB} KB (${sizeMB} MB)`)
console.log(` Path: ${bundlePath}`)
console.log(` Report: ${reportPath}`)
console.log('')
// List bundled modules
if (listModules) {
console.log('\x1b[33mBundled Modules:\x1b[0m')
const content = fs.readFileSync(bundlePath, 'utf-8')
const moduleMatches = content.match(/"\.\/dist\/[^"]+/g) || []
const modules = [...new Set(moduleMatches)]
.map((m) => m.replace(/^"/, ''))
.filter((m) => !m.includes(' recursive'))
.sort()
modules.forEach((m) => console.log(` ${m}`))
console.log(`\n Total: ${modules.length} modules`)
console.log('')
}
// List externalized modules
if (listExternals) {
console.log('\x1b[33mExternalized Modules:\x1b[0m')
const content = fs.readFileSync(bundlePath, 'utf-8')
// Find external requires
const externalMatches =
content.match(
/require\("(next\/dist\/[^"]+|@next\/[^"]+|styled-jsx[^"]*)"\)/g
) || []
const externals = [...new Set(externalMatches)]
.map((m) => m.match(/require\("([^"]+)"\)/)[1])
.sort()
externals.forEach((m) => console.log(` ${m}`))
console.log(`\n Total: ${externals.length} external requires`)
console.log('')
}
// Output JSON stats
if (outputJson) {
const statsJsonPath = path.join(
nextDir,
'dist/compiled/dev-server/stats.json'
)
console.log(`Stats JSON: ${statsJsonPath}`)
console.log('(Run with ANALYZE_REASONS=1 for detailed stats)')
}
// Open in browser
if (openBrowser) {
console.log('Opening report in browser...')
const opener =
process.platform === 'darwin'
? 'open'
: process.platform === 'win32'
? 'start'
: 'xdg-open'
try {
execSync(`${opener} "${reportPath}"`, { stdio: 'ignore' })
} catch {
console.log(`Could not open browser. Open manually: ${reportPath}`)
}
}
console.log('\x1b[32mDone!\x1b[0m')
console.log('')
console.log('Tips:')
console.log(' - Open the HTML report to see interactive treemap')
console.log(' - Use --list-modules to see all bundled modules')
console.log(' - Use --list-externals to see external requires')
console.log(' - Use --verbose for detailed build output')

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env node
/**
* Analyze a CPU profile to identify hot modules
*/
const fs = require('fs')
const profilePath = process.argv[2]
if (!profilePath) {
console.error('Usage: node analyze-profile.js <profile.cpuprofile>')
process.exit(1)
}
const profile = JSON.parse(fs.readFileSync(profilePath, 'utf-8'))
// Extract nodes with their hit counts
const nodes = profile.nodes || []
// Group by file/module
const moduleHits = {}
nodes.forEach((node) => {
const fn = node.callFrame
if (fn && fn.url) {
const url = fn.url
// Extract module name from path
let moduleName = url
if (url.includes('next/dist/')) {
moduleName = url.split('next/dist/')[1]
} else if (url.includes('node_modules/')) {
moduleName = 'node_modules/' + url.split('node_modules/').pop()
}
if (!moduleHits[moduleName]) {
moduleHits[moduleName] = { hits: 0 }
}
moduleHits[moduleName].hits += node.hitCount || 0
}
})
// Sort by hits
const sorted = Object.entries(moduleHits)
.filter(([_, v]) => v.hits > 0)
.sort((a, b) => b[1].hits - a[1].hits)
.slice(0, 40)
console.log('Top 40 modules by CPU time:')
console.log('='.repeat(70))
sorted.forEach(([name, data], i) => {
console.log(`${String(i + 1).padStart(2)}. ${name} (${data.hits} hits)`)
})

View File

@@ -0,0 +1,102 @@
const { promisify } = require('util')
const { Octokit } = require('octokit')
const { exec: execOriginal } = require('child_process')
const exec = promisify(execOriginal)
const {
GITHUB_TOKEN = '',
SCRIPT = '',
BRANCH_NAME = 'unknown',
PR_TITLE = 'Automated update',
PR_BODY = '',
} = process.env
if (!GITHUB_TOKEN) {
console.log('missing GITHUB_TOKEN env')
process.exit(1)
}
if (!SCRIPT) {
console.log('missing SCRIPT env')
process.exit(1)
}
async function main() {
const octokit = new Octokit({ auth: GITHUB_TOKEN })
const branchName = `update/${BRANCH_NAME}-${Date.now()}`
await exec(`node ${SCRIPT}`)
await exec(`git config user.name "nextjs-bot"`)
await exec(`git config user.email "it+nextjs-bot@vercel.com"`)
await exec(`git checkout -b ${branchName}`)
await exec(`git add -A`)
await exec(`git commit --message ${branchName}`)
const changesResult = await exec(`git diff HEAD~ --name-only`)
const changedFiles = changesResult.stdout
.split('\n')
.filter((line) => line.trim())
if (changedFiles.length === 0) {
console.log('No files changed skipping.')
return
}
await exec(`git push origin ${branchName}`)
const repo = 'next.js'
const owner = 'vercel'
const { data: pullRequests } = await octokit.rest.pulls.list({
owner,
repo,
state: 'open',
sort: 'created',
direction: 'desc',
per_page: 100,
})
const pullRequest = await octokit.rest.pulls.create({
owner,
repo,
head: branchName,
base: 'canary',
title: PR_TITLE,
body: PR_BODY,
})
await octokit.rest.issues.addLabels({
owner,
repo,
issue_number: pullRequest.data.number,
labels: ['run-react-18-tests'],
})
console.log('Created pull request', pullRequest.url)
const previousPullRequests = pullRequests.filter(({ title, user }) => {
return title.includes(PR_TITLE) && user.login === 'nextjs-bot'
})
if (previousPullRequests.length) {
for await (const previousPullRequest of previousPullRequests) {
console.log(
`Closing previous pull request: ${previousPullRequest.html_url}`
)
await octokit.rest.pulls.update({
owner,
repo,
pull_number: previousPullRequest.number,
state: 'closed',
})
}
}
}
main().catch((err) => {
console.error(err)
// Ensure the process exists with a non-zero exit code so that the workflow fails
process.exit(1)
})

View File

@@ -0,0 +1,203 @@
#!/bin/bash
#
# Benchmark dev server boot time (wall-clock)
#
# Measures TWO metrics:
# 1. listen_time: When server starts accepting TCP connections
# 2. ready_time: When server responds to first HTTP request
#
# The delta between these shows how much initialization is deferred after "Ready".
#
# Usage:
# ./scripts/benchmark-boot-time.sh [runs] [test-dir]
#
# Examples:
# ./scripts/benchmark-boot-time.sh # 5 runs, uses /tmp/next-boot-test
# ./scripts/benchmark-boot-time.sh 3 # 3 runs
# ./scripts/benchmark-boot-time.sh 5 ./my-app # 5 runs on existing app
set -e
RUNS=${1:-5}
TEST_DIR=${2:-/tmp/next-boot-test}
NEXT_BIN="$(dirname "$0")/../packages/next/dist/bin/next"
PORT=3456
echo "=== Dev Server Boot Time Benchmark ==="
echo "Runs: $RUNS"
echo "Test dir: $TEST_DIR"
echo "Next.js: $NEXT_BIN"
echo ""
echo "Metrics:"
echo " listen_time: TCP port accepting connections"
echo " ready_time: First HTTP request succeeds"
echo " delta: ready_time - listen_time (deferred init)"
echo ""
# Create test app if it doesn't exist
if [ ! -f "$TEST_DIR/package.json" ]; then
echo "Creating test app..."
mkdir -p "$TEST_DIR/app"
cat > "$TEST_DIR/package.json" << 'EOF'
{
"name": "boot-test",
"private": true,
"dependencies": {
"react": "19.0.0",
"react-dom": "19.0.0"
}
}
EOF
cat > "$TEST_DIR/app/layout.tsx" << 'EOF'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html><body>{children}</body></html>
}
EOF
cat > "$TEST_DIR/app/page.tsx" << 'EOF'
export default function Home() { return <h1>Hello</h1> }
EOF
(cd "$TEST_DIR" && npm install --silent)
# Link local next
(cd "$TEST_DIR" && npm link "$(dirname "$NEXT_BIN")/.." 2>/dev/null || true)
fi
# Kill any existing next dev on our port
pkill -f "next dev.*$PORT" 2>/dev/null || true
sleep 0.5
# Returns: listen_time,ready_time (comma-separated)
benchmark_run() {
local label=$1
local clean_next=$2
if [ "$clean_next" = "true" ]; then
rm -rf "$TEST_DIR/.next"
fi
# Measure wall-clock time from command start
local start_time=$(python3 -c 'import time; print(int(time.time() * 1000))')
"$NEXT_BIN" dev --turbopack --port $PORT "$TEST_DIR" > /dev/null 2>&1 &
local pid=$!
local timeout=600 # 30s at 50ms intervals
local listen_time=""
local ready_time=""
# Phase 1: Wait for port to be listening (nc -z)
for i in $(seq 1 $timeout); do
if nc -z localhost $PORT 2>/dev/null; then
listen_time=$(python3 -c 'import time; print(int(time.time() * 1000))')
break
fi
sleep 0.05
done
# Phase 2: Wait for HTTP response (curl)
if [ -n "$listen_time" ]; then
for i in $(seq 1 $timeout); do
if curl -s "http://localhost:$PORT" > /dev/null 2>&1; then
ready_time=$(python3 -c 'import time; print(int(time.time() * 1000))')
break
fi
sleep 0.05
done
fi
# Kill the server
kill $pid 2>/dev/null || true
wait $pid 2>/dev/null || true
if [ -n "$listen_time" ] && [ -n "$ready_time" ]; then
local listen_delta=$((listen_time - start_time))
local ready_delta=$((ready_time - start_time))
echo "$listen_delta,$ready_delta"
else
echo "TIMEOUT,TIMEOUT"
fi
}
run_benchmark_series() {
local series_name=$1
local clean_next=$2
echo "--- $series_name ---"
echo "Run | Listen | Ready | Delta"
echo "----|--------|-------|------"
local listen_times=""
local ready_times=""
local deltas=""
for i in $(seq 1 $RUNS); do
RESULT=$(benchmark_run "$series_name-$i" "$clean_next")
LISTEN=$(echo "$RESULT" | cut -d',' -f1)
READY=$(echo "$RESULT" | cut -d',' -f2)
if [ "$LISTEN" != "TIMEOUT" ] && [ "$READY" != "TIMEOUT" ]; then
DELTA=$((READY - LISTEN))
printf "%3d | %5dms | %5dms | %5dms\n" "$i" "$LISTEN" "$READY" "$DELTA"
listen_times="$listen_times $LISTEN"
ready_times="$ready_times $READY"
deltas="$deltas $DELTA"
else
printf "%3d | TIMEOUT | TIMEOUT | -\n" "$i"
fi
done
# Calculate averages
local listen_avg=$(echo $listen_times | tr ' ' '\n' | grep -v '^$' | awk '{sum+=$1; count++} END {if(count>0) printf "%.0f", sum/count; else print "N/A"}')
local ready_avg=$(echo $ready_times | tr ' ' '\n' | grep -v '^$' | awk '{sum+=$1; count++} END {if(count>0) printf "%.0f", sum/count; else print "N/A"}')
local delta_avg=$(echo $deltas | tr ' ' '\n' | grep -v '^$' | awk '{sum+=$1; count++} END {if(count>0) printf "%.0f", sum/count; else print "N/A"}')
echo ""
echo "Average: listen=${listen_avg}ms, ready=${ready_avg}ms, delta=${delta_avg}ms"
echo ""
# Export for summary
export "${series_name}_LISTEN_AVG=$listen_avg"
export "${series_name}_READY_AVG=$ready_avg"
export "${series_name}_DELTA_AVG=$delta_avg"
}
# Run cold start benchmarks
run_benchmark_series "COLD" true
# Warmup for bytecode cache
echo "--- Warming up bytecode cache (12s) ---"
"$NEXT_BIN" dev --turbopack --port $PORT "$TEST_DIR" > /dev/null 2>&1 &
WARMUP_PID=$!
for i in $(seq 1 200); do
if curl -s "http://localhost:$PORT" > /dev/null 2>&1; then
break
fi
sleep 0.05
done
sleep 12
kill $WARMUP_PID 2>/dev/null || true
wait $WARMUP_PID 2>/dev/null || true
echo ""
# Run warm start benchmarks
run_benchmark_series "WARM" false
# Summary
echo "=============================================="
echo " SUMMARY"
echo "=============================================="
echo ""
echo "Cold Start ($RUNS runs):"
echo " Port listening: ${COLD_LISTEN_AVG}ms"
echo " First request: ${COLD_READY_AVG}ms"
echo " Deferred init: ${COLD_DELTA_AVG}ms"
echo ""
echo "Warm Start ($RUNS runs):"
echo " Port listening: ${WARM_LISTEN_AVG}ms"
echo " First request: ${WARM_READY_AVG}ms"
echo " Deferred init: ${WARM_DELTA_AVG}ms"
echo ""
if [ "$COLD_READY_AVG" != "N/A" ] && [ "$WARM_READY_AVG" != "N/A" ]; then
CACHE_BENEFIT=$((COLD_READY_AVG - WARM_READY_AVG))
echo "Cache benefit: ${CACHE_BENEFIT}ms (cold - warm ready)"
fi

View File

@@ -0,0 +1,245 @@
#!/usr/bin/env node
/**
* Dev Server Boot Time Benchmark
*
* Usage:
* node scripts/benchmark-boot.js [options]
*
* Options:
* --iterations=N Number of iterations (default: 5)
* --test-dir=PATH Test project directory (default: /private/tmp/next-boot-test)
* --bundled Use bundled dev server (default)
* --unbundled Use unbundled dev server
* --compare Run both bundled and unbundled for comparison
* --turbopack Use Turbopack (default)
* --webpack Use Webpack
*/
const { spawn, execSync } = require('child_process')
const path = require('path')
const fs = require('fs')
// Parse arguments
const args = process.argv.slice(2)
const getArg = (name, defaultValue) => {
const arg = args.find((a) => a.startsWith(`--${name}=`))
return arg ? arg.split('=')[1] : defaultValue
}
const hasFlag = (name) => args.includes(`--${name}`)
const iterations = parseInt(getArg('iterations', '5'), 10)
const testDir = getArg('test-dir', '/private/tmp/next-boot-test')
const compare = hasFlag('compare')
const useWebpack = hasFlag('webpack')
const bundlerFlag = useWebpack ? '--webpack' : '--turbopack'
const nextDir = path.join(__dirname, '..', 'packages', 'next')
const nextBin = path.join(nextDir, 'dist/bin/next')
const cliSource = path.join(nextDir, 'src/cli/next-dev.ts')
console.log('\x1b[34m=== Next.js Dev Server Boot Benchmark ===\x1b[0m')
console.log(`Iterations: ${iterations}`)
console.log(`Test directory: ${testDir}`)
console.log(`Bundler: ${useWebpack ? 'Webpack' : 'Turbopack'}`)
console.log('')
// Verify test directory exists
if (!fs.existsSync(testDir)) {
console.error(
`\x1b[31mError: Test directory does not exist: ${testDir}\x1b[0m`
)
console.log('Create a test project first:')
console.log(` mkdir -p ${testDir} && cd ${testDir}`)
console.log(' pnpm init && pnpm add next@canary react react-dom')
console.log(
' mkdir -p app && echo "export default function Page() { return <h1>Hello</h1> }" > app/page.tsx'
)
process.exit(1)
}
// Kill existing next dev processes
function killNextDev() {
try {
execSync('pkill -f "next dev"', { stdio: 'ignore' })
} catch {}
}
// Run a single benchmark iteration
function runIteration() {
return new Promise((resolve, reject) => {
// Clean .next directory
const nextCache = path.join(testDir, '.next')
if (fs.existsSync(nextCache)) {
fs.rmSync(nextCache, { recursive: true, force: true })
}
const startTime = Date.now()
let resolved = false
const child = spawn(nextBin, ['dev', bundlerFlag], {
cwd: testDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, FORCE_COLOR: '0' },
})
let output = ''
const onData = (data) => {
output += data.toString()
// Look for "Ready in Xms" pattern
const match = output.match(/Ready in (\d+)ms/)
if (match && !resolved) {
resolved = true
const reportedTime = parseInt(match[1], 10)
const actualTime = Date.now() - startTime
child.kill('SIGTERM')
resolve({ reportedTime, actualTime })
}
}
child.stdout.on('data', onData)
child.stderr.on('data', onData)
child.on('error', (err) => {
if (!resolved) {
resolved = true
reject(err)
}
})
// Timeout after 60 seconds
setTimeout(() => {
if (!resolved) {
resolved = true
child.kill('SIGKILL')
reject(new Error('Timeout waiting for server to start'))
}
}, 60000)
})
}
// Run benchmark with multiple iterations
async function runBenchmark(name) {
console.log(`\x1b[33mRunning ${name}...\x1b[0m`)
const reportedTimes = []
const actualTimes = []
for (let i = 1; i <= iterations; i++) {
try {
killNextDev()
await new Promise((r) => setTimeout(r, 500))
const { reportedTime, actualTime } = await runIteration()
reportedTimes.push(reportedTime)
actualTimes.push(actualTime)
console.log(
` Run ${i}: ${reportedTime}ms (reported) / ${actualTime}ms (actual)`
)
} catch (err) {
console.log(` Run ${i}: Failed - ${err.message}`)
}
}
killNextDev()
if (reportedTimes.length === 0) {
console.log('\x1b[31mNo successful runs\x1b[0m')
return null
}
// Calculate statistics
const calcStats = (times) => {
const sum = times.reduce((a, b) => a + b, 0)
const avg = Math.round(sum / times.length)
const min = Math.min(...times)
const max = Math.max(...times)
const sorted = [...times].sort((a, b) => a - b)
const median = sorted[Math.floor(sorted.length / 2)]
return { avg, min, max, median, count: times.length }
}
const reported = calcStats(reportedTimes)
const actual = calcStats(actualTimes)
console.log(`\x1b[32mResults for ${name}:\x1b[0m`)
console.log(` Reported time (Next.js internal):`)
console.log(
` Avg: ${reported.avg}ms | Min: ${reported.min}ms | Max: ${reported.max}ms | Median: ${reported.median}ms`
)
console.log(` Actual time (CLI to ready):`)
console.log(
` Avg: ${actual.avg}ms | Min: ${actual.min}ms | Max: ${actual.max}ms | Median: ${actual.median}ms`
)
console.log('')
return { reported, actual }
}
// Switch between bundled/unbundled
function setBundled(useBundled) {
const content = fs.readFileSync(cliSource, 'utf-8')
const bundledPath = `require.resolve(
'../compiled/dev-server/start-server'
)`
const unbundledPath = `require.resolve('../server/lib/start-server')`
let newContent
if (useBundled) {
newContent = content.replace(
/const startServerPath = require\.resolve\(['"]\.\.\/server\/lib\/start-server['"]\)/,
`const startServerPath = ${bundledPath}`
)
} else {
newContent = content.replace(
/const startServerPath = require\.resolve\(\s*['"]\.\.\/compiled\/dev-server\/start-server['"]\s*\)/,
`const startServerPath = ${unbundledPath}`
)
}
if (newContent !== content) {
fs.writeFileSync(cliSource, newContent)
// Rebuild CLI
console.log(`Rebuilding CLI (${useBundled ? 'bundled' : 'unbundled'})...`)
execSync('npx taskr cli', { cwd: nextDir, stdio: 'ignore' })
}
}
// Main
async function main() {
killNextDev()
if (compare) {
// Run both bundled and unbundled
setBundled(true)
const bundledResults = await runBenchmark('Bundled dev server')
setBundled(false)
const unbundledResults = await runBenchmark('Unbundled dev server')
// Restore to bundled
setBundled(true)
// Print comparison
console.log('\x1b[34m=== Comparison ===\x1b[0m')
if (bundledResults && unbundledResults) {
const reportedDiff =
bundledResults.reported.avg - unbundledResults.reported.avg
const actualDiff = bundledResults.actual.avg - unbundledResults.actual.avg
console.log(
`Reported time difference: ${reportedDiff > 0 ? '+' : ''}${reportedDiff}ms (${reportedDiff > 0 ? 'bundled slower' : 'bundled faster'})`
)
console.log(
`Actual time difference: ${actualDiff > 0 ? '+' : ''}${actualDiff}ms (${actualDiff > 0 ? 'bundled slower' : 'bundled faster'})`
)
}
} else {
await runBenchmark('Dev server')
}
console.log('\x1b[32mDone!\x1b[0m')
}
main().catch(console.error)

83
scripts/build-native.ts Normal file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env node
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 } from './pack-util'
const nextSwcDir = path.join(NEXT_DIR, 'packages/next-swc')
export default async function buildNative(
buildNativeArgs: string[]
): Promise<void> {
const buildCommand = ['pnpm', 'run', 'build-native', ...buildNativeArgs]
logCommand('Build native bindings', buildCommand)
await execa(buildCommand[0], buildCommand.slice(1), {
cwd: nextSwcDir,
// Without a shell, `pnpm run build-native` returns a 0 exit code on SIGINT?
shell: true,
env: {
NODE_ENV: process.env.NODE_ENV,
CARGO_TERM_COLOR: 'always',
TTY: '1',
},
stdio: 'inherit',
})
await writeTypes()
}
// Check if this file is being run directly
if (import.meta.url === url.pathToFileURL(process.argv[1]).toString()) {
buildNative(process.argv.slice(2)).catch((err) => {
console.error(err)
process.exit(1)
})
}
async function writeTypes() {
const generatedTypesPath = path.join(
NEXT_DIR,
'packages/next-swc/native/index.d.ts'
)
const vendoredTypesPath = path.join(
NEXT_DIR,
'packages/next/src/build/swc/generated-native.d.ts'
)
const generatedTypesMarker = '// GENERATED-TYPES-BELOW\n'
const generatedNotice =
'// DO NOT MANUALLY EDIT THESE TYPES\n' +
'// You can regenerate this file by running `pnpm swc-build-native` in the root of the repo.\n\n'
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
const prettifyCommand = ['prettier', '--stdin-filepath', vendoredTypesPath]
logCommand('Prettify generated types', prettifyCommand)
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)
}

52
scripts/build-wasm.cjs Normal file
View File

@@ -0,0 +1,52 @@
// This script must be run with tsx
const { NEXT_DIR, execAsyncWithOutput, execFn, exec } = require('./pack-util')
const fs = require('fs')
const path = require('path')
const nextSwcDir = path.join(NEXT_DIR, 'packages/next-swc')
;(async () => {
await execAsyncWithOutput(
'Build wasm bindings',
['pnpm', 'run', 'build-wasm', ...process.argv.slice(2)],
{
cwd: nextSwcDir,
shell: process.platform === 'win32' ? 'powershell.exe' : false,
env: {
CARGO_TERM_COLOR: 'always',
TTY: '1',
...process.env,
},
}
)
execFn(
'Copy generated types to `next/src/build/swc/generated-wasm.d.ts`',
() => writeTypes()
)
})()
function writeTypes() {
const generatedTypesPath = path.join(NEXT_DIR, 'crates/wasm/pkg/wasm.d.ts')
const vendoredTypesPath = path.join(
NEXT_DIR,
'packages/next/src/build/swc/generated-wasm.d.ts'
)
const generatedNotice =
'// DO NOT MANUALLY EDIT THESE TYPES\n// You can regenerate this file by running `pnpm swc-build-wasm` in the root of the repo.\n\n'
const generatedTypes = fs.readFileSync(generatedTypesPath, 'utf8')
const vendoredTypes = generatedNotice + generatedTypes
fs.writeFileSync(vendoredTypesPath, vendoredTypes)
exec('Prettify generated types', [
'pnpm',
'prettier',
'--write',
vendoredTypesPath,
])
}

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -eu
cargo metadata --format-version 1 --no-deps | jq -r -j '[.packages[] | select(.source == null)]'

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -eu
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
WS_CRATES="$("$SCRIPT_DIR"/get-workspace-crates.sh)"
echo "$WS_CRATES" | jq -r -c '[.[] | select(.targets[] | .kind | contains(["bench"])) | .name] | sort | unique' | jq -r -c '[.[] | select(. != "napi" and . != "wasm")]'

26
scripts/check-examples.sh Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
for folder in examples/* ; do
if [ -f "$folder/package.json" ]; then
cat $folder/package.json | jq '
.private = true |
del(.license, .version, .name, .author, .description)
' | sponge $folder/package.json
fi
if [ -f "$folder/tsconfig.json" ]; then
if [ -d "$folder/app" ] || [ -d "$folder/src/app" ]; then
cp packages/create-next-app/templates/app/ts/next-env.d.ts $folder/next-env.d.ts
else
cp packages/create-next-app/templates/default/ts/next-env.d.ts $folder/next-env.d.ts
fi
fi
if [ ! -f "$folder/.gitignore" ]; then
cp packages/create-next-app/templates/default/js/gitignore $folder/.gitignore;
fi
done;
if [[ ! -z $(git status -s) ]];then
echo "Detected changes"
git status
exit 1
fi

View File

@@ -0,0 +1,26 @@
const { execSync } = require('child_process')
const checkIsRelease = async () => {
let commitId = process.argv[2] || ''
// parse only the last string which should be version if
// it's a publish commit
const commitMsg = execSync(
`git log -n 1 --pretty='format:%B'${commitId ? ` ${commitId}` : ''}`
)
.toString()
.trim()
const versionString = commitMsg.split(' ').pop().trim()
const publishMsgRegex = /^v\d{1,}\.\d{1,}\.\d{1,}(-\w{1,}\.\d{1,})?$/
if (publishMsgRegex.test(versionString)) {
console.log(versionString)
process.exit(0)
} else {
console.log('not publish commit', { commitId, commitMsg, versionString })
process.exit(1)
}
}
checkIsRelease()

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
const globOrig = require('glob')
const { promisify } = require('util')
const glob = promisify(globOrig)
function collectPaths(routes, paths = []) {
for (const route of routes) {
if (route.path && !route.redirect) {
paths.push(route.path)
}
if (route.routes) {
collectPaths(route.routes, paths)
}
}
}
async function main() {
const manifest = 'errors/manifest.json'
let hadError = false
const dir = path.dirname(manifest)
const files = await glob(path.join(dir, '**/*.md'))
const manifestData = JSON.parse(await fs.promises.readFile(manifest, 'utf8'))
const paths = []
collectPaths(manifestData.routes, paths)
const missingFiles = files.filter(
(file) => !paths.includes(`/${file}`) && file !== 'errors/template.md'
)
if (missingFiles.length) {
hadError = true
console.log(`Missing paths in ${manifest}:\n${missingFiles.join('\n')}`)
} else {
console.log(`No missing paths in ${manifest}`)
}
for (const filePath of paths) {
if (
!(await fs.promises
.access(path.join(process.cwd(), filePath), fs.constants.F_OK)
.then(() => true)
.catch(() => false))
) {
console.log('Could not find path:', filePath)
hadError = true
}
}
if (hadError) {
throw new Error('missing/incorrect manifest items detected see above')
}
}
main()
.then(() => console.log('success'))
.catch((err) => {
console.error(err)
process.exit(1)
})

View File

@@ -0,0 +1,8 @@
copy node_modules\webpack5\lib\hmr\HotModuleReplacement.runtime.js packages\next\bundles\webpack\packages\
copy node_modules\webpack5\lib\hmr\JavascriptHotModuleReplacement.runtime.js packages\next\bundles\webpack\packages\
copy node_modules\webpack5\hot\lazy-compilation-node.js packages\next\bundles\webpack\packages\
copy node_modules\webpack5\hot\lazy-compilation-web.js packages\next\bundles\webpack\packages\
yarn --cwd packages/next ncc-compiled
rem Make sure to exit with 1 if there are changes after running ncc-compiled
rem step to ensure we get any changes committed

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -e
cd packages/next
cp node_modules/webpack/lib/hmr/HotModuleReplacement.runtime.js src/bundles/webpack/packages/
cp node_modules/webpack/lib/hmr/JavascriptHotModuleReplacement.runtime.js src/bundles/webpack/packages/
cp node_modules/webpack/hot/lazy-compilation-node.js src/bundles/webpack/packages/
cp node_modules/webpack/hot/lazy-compilation-web.js src/bundles/webpack/packages/
pnpm run ncc-compiled
cd ../../
# Make sure to exit with 1 if there are changes after running ncc-compiled
# step to ensure we get any changes committed
if [[ ! -z $(git status -s) ]];then
echo "Detected changes"
git diff -a --stat
exit 1
fi

View File

@@ -0,0 +1,459 @@
#!/usr/bin/env node
/**
* Scans the Rust codebase to find unused turbo-tasks items:
* - #[turbo_tasks::function] → fn definitions
* - #[turbo_tasks::value] → struct/enum definitions
* - #[turbo_tasks::value_trait] → trait definitions
*
* Exit code 0: no unused items found
* Exit code 1: unused items found
*
* Usage: node scripts/check-unused-turbo-tasks.mjs
*/
import { readdir, readFile } from 'node:fs/promises'
import { join, relative } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const ROOT = join(__dirname, '..')
const SCAN_DIRS = [join(ROOT, 'turbopack/crates'), join(ROOT, 'crates')]
const EXCLUDE_DIRS = ['turbo-tasks-macros-tests']
// After seeing an attribute, give up looking for the item it annotates
// if we haven't found it within this many lines.
const MAX_ANNOTATION_DISTANCE = 10
// ---------------------------------------------------------------------------
// Regexes
// ---------------------------------------------------------------------------
// Attribute detection (applied to trimmed lines)
const TT_FUNCTION_RE = /^#\[turbo_tasks::function/
const TT_VALUE_RE = /^#\[turbo_tasks::value(?![_a-zA-Z0-9])/ // not value_impl or value_trait
const TT_VALUE_IMPL_RE = /^#\[turbo_tasks::value_impl/
const TT_VALUE_TRAIT_RE = /^#\[turbo_tasks::value_trait/
// Item-header extraction
const FN_NAME_RE = /\bfn\s+([a-zA-Z_][a-zA-Z0-9_]*)/
const STRUCT_ENUM_RE =
/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:struct|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/
const TRAIT_RE = /^\s*(?:pub\s+)?trait\s+([A-Za-z_][A-Za-z0-9_]*)/
const IMPL_TRAIT_FOR_RE =
/^\s*impl\s*(?:<[^>]*>\s*)?([A-Za-z_][A-Za-z0-9_:]*(?:<[^>]*>)?)\s+for\s+([A-Za-z_][A-Za-z0-9_:]*)/
const IMPL_INHERENT_RE = /^\s*impl\s*(?:<[^>]*>\s*)?([A-Za-z_][A-Za-z0-9_:]*)/
// Usage scanning
const IDENT_RE = /\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/**
* @typedef {{
* name: string,
* kind: 'function' | 'value' | 'value_trait',
* filePath: string,
* line: number,
* context: 'free' | 'inherent_impl' | 'trait_impl' | 'trait_def',
* typeName?: string,
* traitName?: string,
* }} Definition
*/
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Tracks a "pending annotation": an attribute was seen on some line and we are
* scanning forward to find the item it annotates (a fn, struct, enum, or trait).
*
* Returns null when a match is found or the search window expires, along with
* the matched name (or null).
*/
function resolvePending(pending, currentLine, trimmed, itemRe) {
if (pending < 0) return { pending, match: null }
const m = itemRe.exec(trimmed)
if (m) return { pending: -1, match: m[1] }
if (currentLine - pending > MAX_ANNOTATION_DISTANCE)
return { pending: -1, match: null }
return { pending, match: null }
}
/**
* Count unbalanced braces on a line, ignoring string literals and comments.
*/
function countBraces(line) {
let depth = 0
let inString = false
let escape = false
let inChar = false
for (let i = 0; i < line.length; i++) {
const ch = line[i]
if (escape) {
escape = false
continue
}
if (ch === '\\') {
escape = true
continue
}
// Line comment — stop processing
if (!inString && !inChar && ch === '/' && line[i + 1] === '/') break
if (inString) {
if (ch === '"') inString = false
continue
}
if (inChar) {
if (ch === "'") inChar = false
continue
}
if (ch === '"') {
inString = true
continue
}
// Simplified char literal detection ('x')
if (ch === "'" && i + 2 < line.length && line[i + 2] === "'") {
i += 2
continue
}
if (ch === '{') depth++
if (ch === '}') depth--
}
return depth
}
/**
* Format a Definition for display.
*/
function formatDefinition(def, relPath) {
const loc = `${relPath}:${def.line}`
if (def.kind === 'value') return ` ${loc} - value ${def.name}`
if (def.kind === 'value_trait') return ` ${loc} - value_trait ${def.name}`
const contextLabels = {
free: 'free function',
inherent_impl: `method on ${def.typeName}`,
trait_impl: `impl ${def.traitName} for ${def.typeName}`,
trait_def: `trait ${def.traitName} default method`,
}
const ctx = contextLabels[def.context] ?? def.context
return ` ${loc} - fn ${def.name} (${ctx})`
}
// ---------------------------------------------------------------------------
// Phase 0: Discover all .rs files
// ---------------------------------------------------------------------------
async function discoverRsFiles(dirs) {
const files = []
for (const dir of dirs) {
let entries
try {
entries = await readdir(dir, { recursive: true, withFileTypes: false })
} catch {
continue
}
for (const entry of entries) {
if (entry.endsWith('.rs')) {
const parts = entry.split('/')
if (parts.some((p) => EXCLUDE_DIRS.includes(p))) continue
files.push(join(dir, entry))
}
}
}
return files
}
// ---------------------------------------------------------------------------
// Phase 1: Extract definitions
// ---------------------------------------------------------------------------
/**
* Parse a single .rs file for turbo-tasks definitions.
*
* Tracks three kinds of pending annotations:
* - #[turbo_tasks::function] → expects `fn <name>`
* - #[turbo_tasks::value] → expects `struct <name>` or `enum <name>`
* - #[turbo_tasks::value_trait] → expects `trait <name>`
*
* Also maintains a block stack to determine whether a function lives inside a
* #[turbo_tasks::value_impl] or #[turbo_tasks::value_trait] block, which gives
* the function its context (inherent method, trait impl, trait default method).
*/
function parseDefinitions(filePath, content) {
const lines = content.split('\n')
/** @type {Definition[]} */
const definitions = []
let braceDepth = 0
/**
* Stack of impl/trait blocks we're inside (for function context).
* @type {Array<{ startDepth: number, typeName?: string, traitName?: string, implKind: 'inherent' | 'trait' | 'trait_def' }>}
*/
const blockStack = []
// Pending attribute state: line index where the attribute was seen, or -1.
let pendingFn = -1
let pendingValue = -1
let pendingValueTrait = -1
let pendingBlockType = null // 'value_impl' | 'value_trait'
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const trimmed = line.trimStart()
const isComment = trimmed.startsWith('//')
if (!isComment) {
// --- Detect attributes ---
if (TT_VALUE_IMPL_RE.test(trimmed)) pendingBlockType = 'value_impl'
if (TT_VALUE_TRAIT_RE.test(trimmed)) {
pendingBlockType = 'value_trait'
pendingValueTrait = i
}
if (TT_VALUE_RE.test(trimmed)) pendingValue = i
if (TT_FUNCTION_RE.test(trimmed)) pendingFn = i
// --- Resolve pending value/value_trait annotations into definitions ---
{
const r = resolvePending(pendingValue, i, trimmed, STRUCT_ENUM_RE)
pendingValue = r.pending
if (r.match) {
definitions.push({
name: r.match,
kind: 'value',
filePath,
line: i + 1,
context: 'free',
})
}
}
{
const r = resolvePending(pendingValueTrait, i, trimmed, TRAIT_RE)
pendingValueTrait = r.pending
if (r.match) {
definitions.push({
name: r.match,
kind: 'value_trait',
filePath,
line: i + 1,
context: 'free',
})
}
}
// --- Resolve pending function annotations ---
{
const r = resolvePending(pendingFn, i, trimmed, FN_NAME_RE)
pendingFn = r.pending
if (r.match) {
const block =
blockStack.length > 0 ? blockStack[blockStack.length - 1] : null
let context = 'free'
let typeName, traitName
if (block) {
if (block.implKind === 'inherent') {
context = 'inherent_impl'
typeName = block.typeName
} else if (block.implKind === 'trait') {
context = 'trait_impl'
typeName = block.typeName
traitName = block.traitName
} else if (block.implKind === 'trait_def') {
context = 'trait_def'
traitName = block.traitName
}
}
definitions.push({
name: r.match,
kind: 'function',
filePath,
line: i + 1,
context,
...(typeName && { typeName }),
...(traitName && { traitName }),
})
}
}
// --- Resolve pending block headers (value_impl / value_trait) ---
if (pendingBlockType === 'value_impl') {
const traitImplMatch = IMPL_TRAIT_FOR_RE.exec(trimmed)
if (traitImplMatch) {
blockStack.push({
startDepth: braceDepth,
traitName: traitImplMatch[1],
typeName: traitImplMatch[2],
implKind: 'trait',
})
pendingBlockType = null
} else {
const inherentMatch = IMPL_INHERENT_RE.exec(trimmed)
if (inherentMatch && trimmed.includes('{')) {
blockStack.push({
startDepth: braceDepth,
typeName: inherentMatch[1],
implKind: 'inherent',
})
pendingBlockType = null
}
// else: impl line without opening brace yet — keep waiting
}
} else if (pendingBlockType === 'value_trait') {
const traitMatch = TRAIT_RE.exec(trimmed)
if (traitMatch) {
blockStack.push({
startDepth: braceDepth,
traitName: traitMatch[1],
implKind: 'trait_def',
})
pendingBlockType = null
}
}
}
// Track brace depth (even for comment lines — countBraces skips //)
braceDepth += countBraces(line)
// Pop blocks that have closed
while (
blockStack.length > 0 &&
braceDepth <= blockStack[blockStack.length - 1].startDepth
) {
blockStack.pop()
}
}
return definitions
}
// ---------------------------------------------------------------------------
// Phase 2: Find used names
// ---------------------------------------------------------------------------
/**
* Scan all file contents and return the set of definition names that appear
* at least once outside their definition site.
*
* @param {Map<string, string>} fileContents
* @param {Set<string>} names
* @param {Map<string, Set<string>>} definitionLocations name → Set<"path:line">
* @returns {Set<string>}
*/
function findUsedNames(fileContents, names, definitionLocations) {
const used = new Set()
for (const [filePath, content] of fileContents) {
const lines = content.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
if (line.trimStart().startsWith('//')) continue
let match
IDENT_RE.lastIndex = 0
while ((match = IDENT_RE.exec(line)) !== null) {
const name = match[1]
if (used.has(name) || !names.has(name)) continue
// Skip the definition site itself
const defLocs = definitionLocations.get(name)
if (defLocs && defLocs.has(`${filePath}:${i + 1}`)) continue
used.add(name)
if (used.size === names.size) return used
}
}
}
return used
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
const rsFiles = await discoverRsFiles(SCAN_DIRS)
/** @type {Map<string, string>} */
const fileContents = new Map()
await Promise.all(
rsFiles.map(async (filePath) => {
try {
fileContents.set(filePath, await readFile(filePath, 'utf-8'))
} catch {
/* skip unreadable files */
}
})
)
// Parse definitions from all files
/** @type {Definition[]} */
const allDefinitions = []
for (const [filePath, content] of fileContents) {
allDefinitions.push(...parseDefinitions(filePath, content))
}
// Build indexes
const allNames = new Set(allDefinitions.map((d) => d.name))
/** @type {Map<string, Set<string>>} */
const definitionLocations = new Map()
for (const def of allDefinitions) {
const key = `${def.filePath}:${def.line}`
if (!definitionLocations.has(def.name)) {
definitionLocations.set(def.name, new Set())
}
definitionLocations.get(def.name).add(key)
}
// Find which names have external usage
const usedNames = findUsedNames(fileContents, allNames, definitionLocations)
// Collect and sort unused definitions
const unused = allDefinitions
.filter((d) => !usedNames.has(d.name))
.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.line - b.line)
// Report
if (unused.length === 0) {
console.log(
`No unused turbo-tasks items found (${allDefinitions.length} total checked).`
)
process.exit(0)
}
console.log('Unused turbo-tasks items:\n')
for (const def of unused) {
console.log(formatDefinition(def, relative(ROOT, def.filePath)))
}
console.log(
`\nFound ${unused.length} unused turbo-tasks item(s) out of ${allDefinitions.length} total.`
)
process.exit(1)
}
main().catch((err) => {
console.error(err)
process.exit(2)
})

117
scripts/code-freeze.js Normal file
View File

@@ -0,0 +1,117 @@
const authToken = process.env.CODE_FREEZE_TOKEN
if (!authToken) {
throw new Error(`missing CODE_FREEZE_TOKEN env`)
}
const codeFreezeRule = {
context: 'Potentially publish release',
app_id: 15368,
}
async function updateRules(newRules) {
const res = await fetch(
`https://api.github.com/repos/vercel/next.js/branches/canary/protection`,
{
method: 'PUT',
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${authToken}`,
'X-GitHub-Api-Version': '2022-11-28',
},
body: JSON.stringify(newRules),
}
)
if (!res.ok) {
throw new Error(
`Failed to check for rule ${res.status} ${await res.text()}`
)
}
}
async function getCurrentRules() {
const res = await fetch(
`https://api.github.com/repos/vercel/next.js/branches/canary/protection`,
{
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${authToken}`,
'X-GitHub-Api-Version': '2022-11-28',
},
}
)
if (!res.ok) {
throw new Error(
`Failed to check for rule ${res.status} ${await res.text()}`
)
}
const data = await res.json()
return {
required_status_checks: {
strict: data.required_status_checks.strict,
// checks: data.required_status_checks.checks,
contexts: data.required_status_checks.contexts,
},
enforce_admins: data.enforce_admins.enabled,
required_pull_request_reviews: {
dismiss_stale_reviews:
data.required_pull_request_reviews.dismiss_stale_reviews,
require_code_owner_reviews:
data.required_pull_request_reviews.require_code_owner_reviews,
require_last_push_approval:
data.required_pull_request_reviews.require_last_push_approval,
required_approving_review_count:
data.required_pull_request_reviews.required_approving_review_count,
},
restrictions: {
users: data.restrictions.users?.map((user) => user.login) || [],
teams: data.restrictions.teams?.map((team) => team.slug) || [],
apps: data.restrictions.apps?.map((app) => app.slug) || [],
},
}
}
async function main() {
const typeIdx = process.argv.indexOf('--type')
const type = process.argv[typeIdx + 1]
if (type !== 'enable' && type !== 'disable') {
throw new Error(`--type should be enable or disable`)
}
const isEnable = type === 'enable'
const currentRules = await getCurrentRules()
const hasRule = currentRules.required_status_checks.contexts?.some((ctx) => {
return ctx === codeFreezeRule.context
})
console.log(currentRules)
if (isEnable) {
if (hasRule) {
console.log(`Already enabled`)
return
}
currentRules.required_status_checks.contexts.push(codeFreezeRule.context)
await updateRules(currentRules)
console.log('Enabled code freeze')
} else {
if (!hasRule) {
console.log(`Already disabled`)
return
}
currentRules.required_status_checks.contexts =
currentRules.required_status_checks.contexts.filter(
(ctx) => ctx !== codeFreezeRule.context
)
await updateRules(currentRules)
console.log('Disabled code freeze')
}
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

View File

@@ -0,0 +1,166 @@
// @ts-check
const execa = require('execa')
const fs = require('node:fs/promises')
const os = require('node:os')
const path = require('node:path')
async function main() {
const [
githubHeadSha,
tarballDirectory = path.join(os.tmpdir(), 'vercel-nextjs-preview-tarballs'),
] = process.argv.slice(2)
const repoRoot = path.resolve(__dirname, '..')
await fs.mkdir(tarballDirectory, { recursive: true })
// The preview version is set in packages/next/package.json by
// scripts/set-preview-version.js before the build step.
const nextPackageJson = JSON.parse(
await fs.readFile(path.join(repoRoot, 'packages/next/package.json'), 'utf8')
)
const version = nextPackageJson.version
console.info(`Designated version: ${version}`)
const nativePackagesDir = path.join(repoRoot, 'crates/next-napi-bindings/npm')
const platforms = (await fs.readdir(nativePackagesDir)).filter(
(name) => !name.startsWith('.')
)
console.info(`Creating tarballs for next-swc packages`)
const nextSwcPackageNames = new Set()
await Promise.all(
platforms.map(async (platform) => {
const binaryName = `next-swc.${platform}.node`
try {
await fs.cp(
path.join(repoRoot, 'packages/next-swc/native', binaryName),
path.join(nativePackagesDir, platform, binaryName)
)
} catch (error) {
if (error.code === 'ENOENT') {
console.warn(
`Skipping next-swc platform '${platform}' tarball creation because ${binaryName} was never built.`
)
return
}
throw error
}
const manifest = JSON.parse(
await fs.readFile(
path.join(nativePackagesDir, platform, 'package.json'),
'utf8'
)
)
manifest.version = version
await fs.writeFile(
path.join(nativePackagesDir, platform, 'package.json'),
JSON.stringify(manifest, null, 2) + '\n'
)
// By encoding the package name in the directory, vercel-packages can later extract the package name of a tarball from its path when `tarballDirectory` is zipped.
const packDestination = path.join(tarballDirectory, manifest.name)
await fs.mkdir(packDestination, { recursive: true })
const { stdout } = await execa(
'npm',
['pack', '--pack-destination', packDestination],
{
cwd: path.join(nativePackagesDir, platform),
}
)
// tarball name is printed as the last line of npm-pack
const tarballName = stdout.trim().split('\n').pop()
console.info(`Created tarball ${path.join(packDestination, tarballName)}`)
nextSwcPackageNames.add(manifest.name)
})
)
const lernaListJson = await execa('pnpm', [
'--silent',
'lerna',
'list',
'--json',
])
const packages = JSON.parse(lernaListJson.stdout)
const packagesByVersion = new Map()
// vercel-packages finds GH artifacts via the head SHA because that's the only
// API GitHub offers.
for (const packageInfo of packages) {
packagesByVersion.set(
packageInfo.name,
`https://vercel-packages.vercel.app/next/commits/${githubHeadSha}/${packageInfo.name}`
)
}
for (const nextSwcPackageName of nextSwcPackageNames) {
packagesByVersion.set(
nextSwcPackageName,
`https://vercel-packages.vercel.app/next/commits/${githubHeadSha}/${nextSwcPackageName}`
)
}
console.info(`Creating tarballs for regular packages`)
for (const packageInfo of packages) {
if (packageInfo.private) {
continue
}
const packageJsonPath = path.join(packageInfo.location, 'package.json')
const packageJson = await fs.readFile(packageJsonPath, 'utf8')
const manifest = JSON.parse(packageJson)
manifest.version = version
if (packageInfo.name === 'next') {
manifest.optionalDependencies ??= {}
for (const nextSwcPackageName of nextSwcPackageNames) {
manifest.optionalDependencies[nextSwcPackageName] =
packagesByVersion.get(nextSwcPackageName)
}
}
// ensure it depends on packages from this release.
for (const [dependencyName, version] of packagesByVersion) {
if (manifest.dependencies?.[dependencyName] !== undefined) {
manifest.dependencies[dependencyName] = version
}
if (manifest.devDependencies?.[dependencyName] !== undefined) {
manifest.devDependencies[dependencyName] = version
}
if (manifest.peerDependencies?.[dependencyName] !== undefined) {
manifest.peerDependencies[dependencyName] = version
}
if (manifest.optionalDependencies?.[dependencyName] !== undefined) {
manifest.optionalDependencies[dependencyName] = version
}
}
await fs.writeFile(
packageJsonPath,
JSON.stringify(manifest, null, 2) +
// newline will be added by Prettier
'\n'
)
// By encoding the package name in the directory, vercel-packages can later extract the package name of a tarball from its path when `tarballDirectory` is zipped.
const packDestination = path.join(tarballDirectory, manifest.name)
await fs.mkdir(packDestination, { recursive: true })
const { stdout } = await execa(
'npm',
['pack', '--pack-destination', packDestination],
{
cwd: packageInfo.location,
}
)
// tarball name is printed as the last line of npm-pack
const tarballName = stdout.trim().split('\n').pop()
console.info(`Created tarball ${path.join(packDestination, tarballName)}`)
}
console.info(
`When this job is completed, a Next.js preview build will be available under ${packagesByVersion.get('next')}`
)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

View File

@@ -0,0 +1,130 @@
// @ts-check
const fs = require('fs')
const path = require('path')
const execa = require('execa')
async function main() {
const args = process.argv
const branchName = args[args.indexOf('--branch-name') + 1]
const tagName = args[args.indexOf('--tag-name') + 1]
if (!branchName) {
throw new Error('branchName value is missing!')
}
if (!tagName || !tagName.startsWith('v')) {
throw new Error('tagName value is invalid "' + tagName + '"')
}
const githubToken = process.env.RELEASE_BOT_GITHUB_TOKEN
if (!githubToken) {
console.log(`Missing RELEASE_BOT_GITHUB_TOKEN`)
return
}
await execa(
`git remote set-url origin https://nextjs-bot:${githubToken}@github.com/vercel/next.js.git`,
{ stdio: 'inherit', shell: true }
)
await execa(`git config user.name "nextjs-bot"`, {
stdio: 'inherit',
shell: true,
})
await execa(`git config user.email "it+nextjs-bot@vercel.com"`, {
stdio: 'inherit',
shell: true,
})
await execa(`git checkout -b "${branchName}"`, {
stdio: 'inherit',
shell: true,
})
await execa(`git fetch origin ${tagName} --tags`, {
stdio: 'inherit',
shell: true,
})
await execa(`git reset --hard ${tagName}`, {
stdio: 'inherit',
shell: true,
})
const lernaPath = path.join(__dirname, '..', 'lerna.json')
const existingLerna = JSON.parse(
await fs.promises.readFile(lernaPath, 'utf8')
)
existingLerna.command.publish.allowBranch.push(branchName)
await fs.promises.writeFile(lernaPath, JSON.stringify(existingLerna, null, 2))
const buildAndDeployPath = path.join(
__dirname,
'..',
'.github',
'workflows',
'build_and_deploy.yml'
)
const buildAndDeploy = await fs.promises.readFile(buildAndDeployPath, 'utf8')
await fs.promises.writeFile(
buildAndDeployPath,
buildAndDeploy.replace(/refs\/heads\/canary/g, `refs/heads/${branchName}`)
)
const buildAndTestPath = path.join(
__dirname,
'..',
'.github',
'workflows',
'build_and_test.yml'
)
let buildAndTest = await fs.promises.readFile(buildAndTestPath, 'utf8')
buildAndTest = buildAndTest
.replace(`['canary']`, `['${branchName}']`)
.replace(/[\s]{1,}('test-new-tests-.+',)/g, '')
buildAndTest = buildAndTest.replace(
/(^[ \t]*)# test-new-tests-if\n(^[ \t]*)if:.*\n(^[ \t]*)# test-new-tests-end-if/gm,
(_, indent1, indent2, indent3) =>
`${indent1}# test-new-tests-if\n${indent2}if: false\n${indent3}# test-new-tests-end-if`
)
await fs.promises.writeFile(buildAndTestPath, buildAndTest)
await execa(`git add .`, {
stdio: 'inherit',
shell: true,
})
await execa(`git commit -m "setup release branch"`, {
stdio: 'inherit',
shell: true,
})
await execa(`git push origin "${branchName}"`, {
stdio: 'inherit',
shell: true,
})
console.log(`Waiting 5s before updating branch rules`)
await new Promise((resolve) => setTimeout(resolve, 5_000))
const updateEnvironmentRes = await fetch(
'https://api.github.com/repos/vercel/next.js/environments/release-stable/deployment-branch-policies',
{
method: 'POST',
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${githubToken}`,
'X-GitHub-Api-Version': '2022-11-28',
},
body: JSON.stringify({ name: branchName }),
}
)
if (!updateEnvironmentRes.ok) {
console.error(
{ status: updateEnvironmentRes.status },
await updateEnvironmentRes.text()
)
throw new Error(`Failed to update environment branch rules`)
}
console.log(`Successfully updated deployment environment branch rules`)
}
main()

48
scripts/deploy-docs.sh Normal file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
# Determine production flag from DEPLOY_ENVIRONMENT
PROD=""
if [ "${DEPLOY_ENVIRONMENT:-preview}" = "production" ]; then
PROD="--prod"
fi
if [ -z "${VERCEL_API_TOKEN:-}" ]; then
echo "VERCEL_API_TOKEN was not providing, skipping..." >&2
exit 0
fi
CWD="."
PROJECT="next-docs"
echo "Preparing local build for docs (project: $PROJECT)..." >&2
# Ensure corepack and install only the docs workspace graph
if ! command -v corepack >/dev/null 2>&1; then
echo "Installing corepack..." >&2
npm i -g corepack@0.31 1>&2
fi
corepack enable 1>&2
echo "Installing dependencies for ./apps/docs..." >&2
# Reduce CI side-effects from deps we don't need for docs build
export NEXT_SKIP_NATIVE_POSTINSTALL=1
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
echo "Installing Vercel CLI..." >&2
npm i -g vercel@latest 1>&2
echo "Linking Vercel project..." >&2
vercel link --cwd "$CWD" --scope vercel --project "$PROJECT" --token "$VERCEL_API_TOKEN" --yes 1>&2
echo "Pulling env for $DEPLOY_ENVIRONMENT..." >&2
vercel pull --cwd "$CWD" --yes --environment="${DEPLOY_ENVIRONMENT:-preview}" --token="$VERCEL_API_TOKEN" 1>&2
echo "Building locally with Vercel..." >&2
vercel build --cwd "$CWD" --token="$VERCEL_API_TOKEN" 1>&2
echo "Deploying prebuilt output..." >&2
URL=$(vercel deploy --cwd "$CWD" --prebuilt --archive=tgz --token "$VERCEL_API_TOKEN" $PROD)
echo "$URL"

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
#CHANGED_EXAMPLES=$(node scripts/run-for-change.mjs --type deploy-examples --listChangedDirectories)
##### TODO: fix the script above so it can work for stable releases which reach back multiple commits
CHANGED_EXAMPLES="examples/image-component" # always deploy as a workaround
PROD=""
if [ "$DEPLOY_ENVIRONMENT" = "production" ]; then
PROD="--prod"
fi
if [ -z "$VERCEL_API_TOKEN" ]; then
echo "VERCEL_API_TOKEN was not providing, skipping..."
exit 0
fi
for CWD in $CHANGED_EXAMPLES ; do
HYPHENS=$(echo "$CWD" | tr '/' '-')
PROJECT="nextjs-$HYPHENS"
echo "Deploying directory $CWD as $PROJECT to Vercel..."
vercel link --cwd "$CWD" --scope vercel --project "$PROJECT" --token "$VERCEL_API_TOKEN" --yes
vercel deploy --cwd "$CWD" --token "$VERCEL_API_TOKEN" $PROD
done;

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
PACKAGES="-p next-napi-bindings -p next-api -p next-build -p next-core -p next-custom-transforms"
RUSTDOCFLAGS="-Z unstable-options --enable-index-page" cargo doc $PACKAGES --no-deps

631
scripts/devlow-bench.mjs Normal file
View File

@@ -0,0 +1,631 @@
import { rm, writeFile, readFile } from 'node:fs/promises'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe } from '@vercel/devlow-bench'
import * as devlow from '@vercel/devlow-bench'
import { newBrowserSession } from '@vercel/devlow-bench/browser'
import { command } from '@vercel/devlow-bench/shell'
import { waitForFile } from '@vercel/devlow-bench/file'
const REPO_ROOT = fileURLToPath(new URL('..', import.meta.url))
const START_SERVER_REGEXP = /Ready in \d+/
const URL_REGEXP = /Local:\s+(?<url>.+)\n/
const STARTUP_WAIT_TIMEOUT_MS = 5 * 60 * 1000
const GIT_SHA =
process.env.GITHUB_SHA ??
(await (async () => {
const cmd = command('git', ['rev-parse', 'HEAD'])
await cmd.ok()
return cmd.output
})())
const GIT_BRANCH =
process.env.GITHUB_REF_NAME ??
(await (async () => {
const cmd = command('git', ['rev-parse', '--abbrev-ref', 'HEAD'])
await cmd.ok()
return cmd.output
})())
const nextBuildWorkflow =
(benchmarkName, benchDir, pages, enableTurbopackCache) =>
async ({ turbopack, page }) => {
const pageConfig =
typeof pages[page] === 'string' ? { url: pages[page] } : pages[page]
const cleanupTasks = []
try {
const env = {
TURBO_CACHE: enableTurbopackCache ? '1' : '0',
PATH: process.env.PATH,
NODE: process.env.NODE,
HOSTNAME: process.env.HOSTNAME,
PWD: process.env.PWD,
NEXT_TRACE_UPLOAD_DISABLED: 'true',
NEXT_PRIVATE_SKIP_CANARY_CHECK: 'true',
// Enable next.js test mode to get HMR events and silence canary only
__NEXT_TEST_MODE: '1',
}
const serverEnv = {
...env,
PORT: '0',
}
const benchmarkDir = resolve(REPO_ROOT, 'bench', benchDir)
// cleanup .next directory to remove filesystem cache
await retry(() =>
rm(join(benchmarkDir, '.next'), { recursive: true, force: true })
)
await measureTime('cleanup', {
scenario: benchmarkName,
props: { turbopack, page },
})
const buildArgs = [turbopack ? 'build-turbopack' : 'build-webpack']
let buildShell = command('pnpm', buildArgs, {
cwd: benchmarkDir,
env,
})
await buildShell.ok()
await measureTime('build', {
scenario: benchmarkName,
props: { turbopack, page },
})
// startup browser
let session = await newBrowserSession({})
const closeSession = async () => {
if (session) {
await session.close()
session = null
}
}
cleanupTasks.push(closeSession)
await measureTime('browser startup', {
props: { turbopack, page },
})
// run command to start dev server
const startArgs = [turbopack ? 'start-turbopack' : 'start-webpack']
let shell = command('pnpm', startArgs, {
cwd: benchmarkDir,
env: serverEnv,
})
const killShell = async () => {
if (shell) {
await shell.kill()
shell = null
}
}
cleanupTasks.push(killShell)
// wait for server to be ready
const {
groups: { url },
} = await shell.waitForOutput(URL_REGEXP, {
timeoutMs: STARTUP_WAIT_TIMEOUT_MS,
})
// wait for server to be ready
await shell.waitForOutput(START_SERVER_REGEXP, {
timeoutMs: STARTUP_WAIT_TIMEOUT_MS,
})
await measureTime('server startup', { props: { turbopack, page } })
await shell.reportMemUsage('mem usage after startup', {
props: { turbopack, page },
})
// open page
const pageInstance = await session.hardNavigation(
'open page',
url + pageConfig.url
)
await shell.reportMemUsage('mem usage after open page')
let status = 0
try {
if (
await pageInstance.evaluate(
'!next.appDir && __NEXT_DATA__.page === "/404"'
)
) {
status = 2
}
} catch (e) {
status = 2
}
try {
if (
!(await pageInstance.evaluate(
'next.appDir || __NEXT_DATA__.page && !__NEXT_DATA__.err'
))
) {
status = 1
}
} catch (e) {
status = 1
}
await reportMeasurement('page status', status, 'status code')
// reload page
await session.reload('reload page')
await reportMeasurement(
'console output',
shell.output.split(/\n/).length,
'lines'
)
// close browser
await killShell()
await closeSession()
await measureTime('before build with cache', {
scenario: benchmarkName,
props: { turbopack, page },
})
buildShell = command('pnpm', buildArgs, {
cwd: benchmarkDir,
env,
})
await buildShell.ok()
await measureTime('build with cache', {
scenario: benchmarkName,
props: { turbopack, page },
})
// startup new browser
session = await newBrowserSession({})
await measureTime('browser startup', {
props: { turbopack, page },
})
// run command to start dev server
shell = command('pnpm', startArgs, {
cwd: benchmarkDir,
env: serverEnv,
})
// wait for server to be ready
const {
groups: { url: url2 },
} = await shell.waitForOutput(URL_REGEXP, {
timeoutMs: STARTUP_WAIT_TIMEOUT_MS,
})
await shell.reportMemUsage('mem usage after startup with cache')
// open page
await session.hardNavigation(
'open page with cache',
url2 + pageConfig.url
)
await reportMeasurement(
'console output with cache',
shell.output.split(/\n/).length,
'lines'
)
await shell.reportMemUsage('mem usage after open page with cache')
} catch (e) {
throw e
} finally {
// This must run in order
for (const task of cleanupTasks.reverse()) await task()
await measureTime('shutdown')
}
}
const nextDevWorkflow =
(benchmarkName, benchDir, pages) =>
async ({ turbopack, page }) => {
const pageConfig =
typeof pages[page] === 'string' ? { url: pages[page] } : pages[page]
const cleanupTasks = []
try {
const benchmarkDir = resolve(REPO_ROOT, 'bench', benchDir)
// cleanup .next directory to remove filesystem cache
await retry(() =>
rm(join(benchmarkDir, '.next'), { recursive: true, force: true })
)
await measureTime('cleanup', {
scenario: benchmarkName,
props: { turbopack, page },
})
// startup browser
let session = await newBrowserSession({})
const closeSession = async () => {
if (session) {
await session.close()
session = null
}
}
cleanupTasks.push(closeSession)
await measureTime('browser startup', {
props: { turbopack, page },
})
const env = {
PATH: process.env.PATH,
NODE: process.env.NODE,
HOSTNAME: process.env.HOSTNAME,
PWD: process.env.PWD,
// Disable otel initialization to prevent pending / hanging request to otel collector
OTEL_SDK_DISABLED: 'true',
NEXT_PUBLIC_OTEL_SENTRY: 'true',
NEXT_PUBLIC_OTEL_DEV_DISABLED: 'true',
NEXT_TRACE_UPLOAD_DISABLED: 'true',
// Enable next.js test mode to get HMR events and silence canary only
__NEXT_TEST_MODE: '1',
}
const serverEnv = {
...env,
PORT: '0',
}
// run command to start dev server
const args = [turbopack ? 'dev-turbopack' : 'dev-webpack']
let shell = command('pnpm', args, {
cwd: benchmarkDir,
env: serverEnv,
})
const killShell = async () => {
if (shell) {
await shell.kill()
shell = null
}
}
cleanupTasks.push(killShell)
// wait for server to be ready
const {
groups: { url },
} = await shell.waitForOutput(URL_REGEXP, {
timeoutMs: STARTUP_WAIT_TIMEOUT_MS,
})
// wait for server to be ready
await shell.waitForOutput(START_SERVER_REGEXP, {
timeoutMs: STARTUP_WAIT_TIMEOUT_MS,
})
await measureTime('server startup', { props: { turbopack, page } })
await shell.reportMemUsage('mem usage after startup', {
props: { turbopack, page },
})
// open page
const pageInstance = await session.hardNavigation(
'open page',
url + pageConfig.url
)
await shell.reportMemUsage('mem usage after open page')
let status = 0
try {
if (
await pageInstance.evaluate(
'!next.appDir && __NEXT_DATA__.page === "/404"'
)
) {
status = 2
}
} catch (e) {
status = 2
}
try {
if (
!(await pageInstance.evaluate(
'next.appDir || __NEXT_DATA__.page && !__NEXT_DATA__.err'
))
) {
status = 1
}
} catch (e) {
status = 1
}
await reportMeasurement('page status', status, 'status code')
// reload page
await session.reload('reload page')
await reportMeasurement(
'console output',
shell.output.split(/\n/).length,
'lines'
)
// HMR
if (pageConfig.hmr) {
let hmrEvent = () => {}
pageInstance.exposeBinding(
'TURBOPACK_HMR_EVENT',
(_source, latency) => {
hmrEvent(latency)
}
)
const { file, before, after } = pageConfig.hmr
const path = resolve(benchmarkDir, file)
const content = await readFile(path, 'utf8')
cleanupTasks.push(async () => {
await writeFile(path, content, 'utf8')
})
let currentContent = content
for (let hmrAttempt = 0; hmrAttempt < 10; hmrAttempt++) {
if (hmrAttempt > 0) {
await new Promise((resolve) => {
setTimeout(resolve, 1000)
})
}
const linesStart = shell.output.split(/\n/).length
let reportedName
if (hmrAttempt < 3) {
reportedName = 'hmr/warmup'
} else {
reportedName = 'hmr'
}
await pageInstance.evaluate(
'window.__NEXT_HMR_CB = (arg) => TURBOPACK_HMR_EVENT(arg); window.__NEXT_HMR_LATENCY_CB = (arg) => TURBOPACK_HMR_EVENT(arg);'
)
// eslint-disable-next-line no-loop-func
const hmrDone = new Promise((resolve) => {
let once = true
const end = async (code) => {
const success = code <= 1
if (!success && !reportedName) reportedName = 'hmr'
if (reportedName) {
await reportMeasurement(
`${reportedName}/status`,
code,
'status code'
)
}
clearTimeout(timeout)
resolve(success)
}
cleanupTasks.push(async () => {
if (!once) return
once = false
await end(3)
})
const timeout = setTimeout(async () => {
if (!once) return
once = false
await end(2)
}, 60000)
hmrEvent = async (latency) => {
if (!once) return
once = false
if (reportedName) {
if (typeof latency === 'number') {
await reportMeasurement(
`${reportedName}/reported latency`,
latency,
'ms'
)
}
await measureTime(reportedName, {
relativeTo: `${reportedName}/start`,
})
}
await end(0)
}
pageInstance.once('load', async () => {
if (!once) return
once = false
if (reportedName) {
await measureTime(reportedName, {
relativeTo: `${reportedName}/start`,
})
}
await end(1)
})
})
const idx = before
? currentContent.indexOf(before)
: currentContent.indexOf(after) + after.length
let newContent = `${currentContent}\n\n/* HMR */`
if (file.endsWith('.tsx')) {
newContent = `${currentContent.slice(
0,
idx
)}<div id="hmr-test">HMR</div>${currentContent.slice(idx)}`
} else if (file.endsWith('.css')) {
newContent = `${currentContent.slice(
0,
idx
)}\n--hmr-test-${hmrAttempt}: 0;\n${currentContent.slice(idx)}`
} else if (file.endsWith('.mdx')) {
newContent = `${currentContent.slice(
0,
idx
)}\n\nHMR\n\n${currentContent.slice(idx)}`
}
if (reportedName) {
await measureTime(`${reportedName}/start`)
}
if (currentContent === newContent) {
throw new Error("HMR didn't change content")
}
await writeFile(path, newContent, 'utf8')
currentContent = newContent
const success = await hmrDone
if (reportedName) {
await reportMeasurement(
`console output/${reportedName}`,
shell.output.split(/\n/).length - linesStart,
'lines'
)
}
if (!success) break
}
}
if (turbopack) {
// close dev server and browser
await killShell()
await closeSession()
} else {
// wait for filesystem cache to be written
const waitPromise = new Promise((resolve) => {
setTimeout(resolve, 5000)
})
const cacheLocation = join(
benchmarkDir,
'.next',
'dev',
'cache',
'webpack',
'client-development'
)
await Promise.race([
waitForFile(join(cacheLocation, 'index.pack')),
waitForFile(join(cacheLocation, 'index.pack.gz')),
])
await measureTime('cache created')
await waitPromise
await measureTime('waiting')
// close dev server and browser
await killShell()
await closeSession()
}
// startup new browser
session = await newBrowserSession({})
await measureTime('browser startup', {
props: { turbopack, page },
})
// run command to start dev server
shell = command('pnpm', args, {
cwd: benchmarkDir,
env: serverEnv,
})
// wait for server to be ready
const {
groups: { url: url2 },
} = await shell.waitForOutput(URL_REGEXP, {
timeoutMs: STARTUP_WAIT_TIMEOUT_MS,
})
await shell.reportMemUsage('mem usage after startup with cache')
// open page
await session.hardNavigation(
'open page with cache',
url2 + pageConfig.url
)
await reportMeasurement(
'console output with cache',
shell.output.split(/\n/).length,
'lines'
)
await shell.reportMemUsage('mem usage after open page with cache')
} finally {
// This must run in order
for (const task of cleanupTasks.reverse()) await task()
await measureTime('shutdown')
}
}
const pages = {
homepage: {
url: '/',
hmr: {
file: 'components/lodash.js',
before: '<h1>Client Component</h1>',
},
},
}
describe(
'heavy-npm-deps-dev',
{
turbopack: true,
mode: 'dev',
page: Object.keys(pages),
},
nextDevWorkflow('heavy-npm-deps', 'heavy-npm-deps', pages)
)
describe(
'heavy-npm-deps-build',
{
turbopack: true,
mode: 'build',
page: Object.keys(pages),
},
nextBuildWorkflow('heavy-npm-deps', 'heavy-npm-deps', pages, false)
)
describe(
'heavy-npm-deps-build-turbo-cache-enabled',
{
turbopack: true,
mode: 'build',
page: Object.keys(pages),
},
nextBuildWorkflow(
'heavy-npm-deps-build-turbo-cache-enabled',
'heavy-npm-deps',
pages,
true
)
)
async function retry(fn) {
let lastError
for (let i = 100; i < 2000; i += 100) {
try {
await fn()
return
} catch (e) {
lastError = e
await new Promise((resolve) => {
setTimeout(resolve, i)
})
}
}
throw lastError
}
function measureTime(name, options) {
return devlow.measureTime(name, {
props: {
git_sha: GIT_SHA,
git_branch: GIT_BRANCH,
...options?.props,
},
...options,
})
}
function reportMeasurement(name, value, unit, options) {
return devlow.reportMeasurement(name, value, unit, {
props: {
git_sha: GIT_SHA,
git_branch: GIT_BRANCH,
...options?.props,
},
...options,
})
}

View File

@@ -0,0 +1,118 @@
// @ts-check
import fetch from 'node-fetch'
export async function main() {
const releasesArray = await fetch(
'https://api.github.com/repos/vercel/next.js/releases?per_page=100'
).then((r) => r.json())
const allReleases = releasesArray
.map(({ id, tag_name, created_at, body }) => ({
id,
tag_name,
created_at,
body: body
.replace(/\r\n/g, '\n')
.split('\n')
.map((e) => e.trim()),
}))
.sort((a, b) => a.created_at.localeCompare(b.created_at))
// targetVersion format is `13.4.15-`, generating changes for all 13.4.15-* canary releases
const targetVersion = /v(.*?-)/
.exec(allReleases.filter((e) => /v.*?-/.exec(e.tag_name)).pop().tag_name)
.pop()
const releases = allReleases.filter((v) => v.tag_name.includes(targetVersion))
const lineItems = {
'### Core Changes': [],
'### Minor Changes': [],
'### Documentation Changes': [],
'### Example Changes': [],
'### Misc Changes': [],
'### Patches': [],
'### Credits': [],
}
Object.keys(lineItems).forEach((header) => {
releases.forEach((release) => {
const headerIndex = release.body.indexOf(header)
if (!~headerIndex) return
let headerLastIndex = release.body
.slice(headerIndex + 1)
.findIndex((v) => v.startsWith('###'))
if (~headerLastIndex) {
headerLastIndex = headerLastIndex + headerIndex
} else {
headerLastIndex = release.body.length - 1
}
if (header === '### Credits') {
release.body.slice(headerIndex, headerLastIndex + 1).forEach((e) => {
const re = /@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}/gi
let m
do {
m = re.exec(e)
if (m) {
lineItems[header].push(m.pop())
}
} while (m)
})
} else {
release.body.slice(headerIndex, headerLastIndex + 1).forEach((e) => {
if (!e.startsWith('-')) {
return
}
lineItems[header].push(e)
})
}
})
})
let finalMessage = []
Object.keys(lineItems).forEach((header) => {
let items = lineItems[header]
if (!items.length) {
return
}
finalMessage.push(header)
finalMessage.push('')
if (header === '### Credits') {
items = [...new Set(items)]
let creditsMessage = `Huge thanks to `
if (items.length > 1) {
creditsMessage += items.slice(0, items.length - 1).join(`, `)
creditsMessage += `, and `
}
creditsMessage += items[items.length - 1]
creditsMessage += ` for helping!`
finalMessage.push(creditsMessage)
} else {
items.forEach((e) => finalMessage.push(e))
}
finalMessage.push('')
})
return {
version: targetVersion.slice(0, -1),
firstVersion: releases[0].tag_name,
lastVersion: releases[releases.length - 1].tag_name,
content: finalMessage.join('\n'),
}
}
main().then((result) => {
console.log(result.content)
})

View File

@@ -0,0 +1,87 @@
// @ts-check
import fs from 'fs/promises'
import execa from 'execa'
import path from 'path'
import { getDiffRevision, getGitInfo } from './git-info.mjs'
/**
* Detects changed tests files by comparing the current branch with `origin/canary`
* Returns tests separated by test mode (dev/prod), as well as the corresponding commit hash
* that the current branch is pointing to
*/
export default async function getChangedTests() {
/** @type import('execa').Options */
const EXECA_OPTS = { shell: true }
const { branchName, remoteUrl, commitSha, isCanary } = await getGitInfo()
if (isCanary) {
console.log(`Skipping flake detection for canary`)
return { devTests: [], prodTests: [], deployTests: [] }
}
const diffRevision = await getDiffRevision()
const changesResult = await execa(
`git diff ${diffRevision} --name-only`,
EXECA_OPTS
).catch((err) => {
console.error(err)
return { stdout: '', stderr: '' }
})
console.log(
{
branchName,
remoteUrl,
isCanary,
commitSha,
},
`\ngit diff:\n${changesResult.stderr}\n${changesResult.stdout}`
)
const changedFiles = changesResult.stdout.split('\n')
// run each test 3 times in each test mode (if E2E) with no-retrying
// and if any fail it's flakey
const devTests = []
const prodTests = []
const deployTests = []
for (let file of changedFiles) {
// normalize slashes
file = file.replace(/\\/g, '/')
const fileExists = await fs
.access(path.join(process.cwd(), file), fs.constants.F_OK)
.then(() => true)
.catch(() => false)
if (fileExists && file.match(/^test\/.*?\.test\.(js|ts|tsx)$/)) {
if (file.startsWith('test/e2e/')) {
devTests.push(file)
prodTests.push(file)
deployTests.push(file)
} else if (file.startsWith('test/integration/')) {
devTests.push(file)
prodTests.push(file)
} else if (file.startsWith('test/prod')) {
prodTests.push(file)
} else if (file.startsWith('test/development')) {
devTests.push(file)
}
}
}
console.log(
'Detected tests:',
JSON.stringify(
{
devTests,
prodTests,
deployTests,
},
null,
2
)
)
return { devTests, prodTests, deployTests, commitSha }
}

19
scripts/git-configure.mjs Normal file
View File

@@ -0,0 +1,19 @@
// @ts-check
import execa from 'execa'
// See https://github.com/vercel/next.js/pull/47375
await execa('git', ['config', 'index.skipHash', 'false'], {
stdio: 'inherit',
reject: false,
})
// Enable the errors.json git merge driver.
// It can be disabled by running:
//
// scripts/merge-errors-json/uninstall
//
// or by manually removing the `[merge "errors-json"]` section from your .git/config.
await execa('scripts/merge-errors-json/install', [], {
stdio: 'inherit',
reject: false,
})

79
scripts/git-info.mjs Normal file
View File

@@ -0,0 +1,79 @@
// @ts-check
import fs from 'fs/promises'
import { promisify } from 'util'
import { exec as execOrig } from 'child_process'
const exec = promisify(execOrig)
/**
* Gets git repository information from the environment
* @returns {Promise<{branchName: string, remoteUrl: string, commitSha: string, isCanary: boolean}>}
*/
export async function getGitInfo() {
let eventData = {}
try {
eventData =
JSON.parse(
await fs.readFile(process.env.GITHUB_EVENT_PATH || '', 'utf8')
)['pull_request'] || {}
} catch (_) {}
const branchName =
eventData?.head?.ref ||
process.env.GITHUB_REF_NAME ||
(await exec('git rev-parse --abbrev-ref HEAD')).stdout.trim()
const remoteUrl =
eventData?.head?.repo?.full_name ||
process.env.GITHUB_REPOSITORY ||
(await exec('git remote get-url origin')).stdout.trim()
const commitSha =
eventData?.head?.sha ||
process.env.GITHUB_SHA ||
(await exec('git rev-parse HEAD')).stdout.trim()
const isCanary =
branchName === 'canary' && remoteUrl.includes('vercel/next.js')
return { branchName, remoteUrl, commitSha, isCanary }
}
/**
* Determines the appropriate git diff revision based on the environment
* @returns {Promise<string>} The git revision to diff against
*/
export async function getDiffRevision() {
if (
process.env.GITHUB_ACTIONS === 'true' &&
process.env.GITHUB_EVENT_NAME === 'pull_request'
) {
// GH Actions for `pull_request` run on the merge commit so HEAD~1:
// 1. includes all changes in the PR
// e.g. in
// A-B-C-main - F
// \ /
// D-E-branch
// GH actions for `branch` runs on F, so a diff for HEAD~1 includes the diff of D and E combined
// 2. Includes all changes of the commit for pushes
return 'HEAD~1'
} else {
try {
await exec('git remote set-branches --add origin canary')
await exec('git fetch origin canary --depth=20')
} catch (err) {
const remoteInfo = await exec('git remote -v')
console.error(remoteInfo.stdout)
console.error(remoteInfo.stderr)
console.error(`Failed to fetch origin/canary`, err)
}
// TODO: We should diff against the merge base with origin/canary not directly against origin/canary.
// A --- B ---- origin/canary
// \
// \-- C ---- HEAD
// `git diff origin/canary` includes B and C
// But we should only include C.
return 'origin/canary'
}
}

View File

@@ -0,0 +1,75 @@
const path = require('path')
const { spawn } = require('child_process')
const fs = require('fs/promises')
const cwd = process.cwd()
async function main() {
const tarballs = await fs.readdir(path.join(cwd, 'public'))
const nextTarball = tarballs.find((item) => !item.includes('-swc'))
await fs.rename(
path.join(cwd, 'public', nextTarball),
path.join(cwd, nextTarball)
)
await new Promise((resolve, reject) => {
const child = spawn('tar', ['-xf', nextTarball], {
stdio: 'inherit',
shell: true,
cwd,
})
child.on('exit', (code) => {
if (code) {
return reject(`Failed with code ${code}`)
}
resolve()
})
})
const unpackedPackageJson = path.join(cwd, 'package/package.json')
const parsedPackageJson = JSON.parse(
await fs.readFile(unpackedPackageJson, 'utf8')
)
const { optionalDependencies } = parsedPackageJson
for (const key of Object.keys(optionalDependencies)) {
optionalDependencies[key] = optionalDependencies[key].replace(
'DEPLOY_URL',
process.env.VERCEL_URL
)
}
await fs.writeFile(
unpackedPackageJson,
JSON.stringify(parsedPackageJson, null, 2)
)
await fs.unlink(nextTarball)
await new Promise((resolve, reject) => {
const child = spawn('tar', ['-czf', nextTarball, 'package'], {
stdio: 'inherit',
shell: true,
cwd,
})
child.on('exit', (code) => {
if (code) {
return reject(`Failed with code ${code}`)
}
resolve()
})
})
await fs.rename(
path.join(cwd, nextTarball),
path.join(cwd, 'public', nextTarball)
)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

View File

@@ -0,0 +1,91 @@
import os from 'os'
import path from 'path'
import execa from 'execa'
import fs from 'fs'
import fsp from 'fs/promises'
;(async function () {
if (process.env.NEXT_SKIP_NATIVE_POSTINSTALL) {
console.log(
`Skipping next-swc postinstall due to NEXT_SKIP_NATIVE_POSTINSTALL env`
)
return
}
const preferOffline = process.env.NEXT_TEST_PREFER_OFFLINE === '1'
let cwd = process.cwd()
const { version: nextVersion } = JSON.parse(
fs.readFileSync(path.join(cwd, 'packages', 'next', 'package.json'))
)
const { packageManager } = JSON.parse(
fs.readFileSync(path.join(cwd, 'package.json'))
)
try {
// if installed swc package version matches monorepo version
// we can skip re-installing
for (const pkg of fs.readdirSync(path.join(cwd, 'node_modules', '@next'))) {
if (
pkg.startsWith('swc-') &&
JSON.parse(
fs.readFileSync(
path.join(cwd, 'node_modules', '@next', pkg, 'package.json')
)
).version === nextVersion
) {
console.log(`@next/${pkg}@${nextVersion} already installed, skipping`)
return
}
}
} catch {}
try {
let tmpdir = path.join(os.tmpdir(), `next-swc-${Date.now()}`)
fs.mkdirSync(tmpdir, { recursive: true })
let pkgJson = {
name: 'dummy-package',
version: '1.0.0',
optionalDependencies: {
'@next/swc-darwin-arm64': nextVersion,
'@next/swc-darwin-x64': nextVersion,
'@next/swc-linux-arm64-gnu': nextVersion,
'@next/swc-linux-arm64-musl': nextVersion,
'@next/swc-linux-x64-gnu': nextVersion,
'@next/swc-linux-x64-musl': nextVersion,
'@next/swc-win32-arm64-msvc': nextVersion,
'@next/swc-win32-x64-msvc': nextVersion,
},
packageManager,
}
fs.writeFileSync(path.join(tmpdir, 'package.json'), JSON.stringify(pkgJson))
fs.writeFileSync(path.join(tmpdir, '.npmrc'), 'node-linker=hoisted')
const args = ['add', `next@${nextVersion}`]
if (preferOffline) {
args.push('--prefer-offline')
}
let { stdout } = await execa('pnpm', args, { cwd: tmpdir })
console.log(stdout)
let pkgs = fs.readdirSync(path.join(tmpdir, 'node_modules/@next'))
fs.mkdirSync(path.join(cwd, 'node_modules/@next'), { recursive: true })
await Promise.all(
pkgs.map(async (pkg) => {
const from = path.join(tmpdir, 'node_modules/@next', pkg)
const to = path.join(cwd, 'node_modules/@next', pkg)
// The directory from pnpm store is a symlink, which can not be overwritten,
// so we remove the existing directory before copying
await fsp.rm(to, { recursive: true, force: true })
// Renaming is flaky on Windows, and the tmpdir is going to be deleted anyway,
// so we use copy the directory instead
return fsp.cp(from, to, { force: true, recursive: true })
})
)
fs.rmSync(tmpdir, { recursive: true, force: true })
console.log('Installed the following binary packages:', pkgs)
} catch (e) {
console.error(e)
console.error('Failed to load @next/swc binary packages')
}
})()

22
scripts/macos-compress.sh Normal file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
#
# Optionally automates compressing target/ and other directories.
# Only intended for and needed on macOS.
#
set -e
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
NEXT_DIR=$(realpath "$SCRIPT_DIR/..")
# Basic timestamp prefix for logging
PS4='+ $(date "+%Y-%m-%d +%H:%M:%S") '
set -x
if ! command -v afsctool &> /dev/null; then
echo "afsctool is required. Install it with 'brew install afsctool'."
exit 1
fi
afsctool -c "$NEXT_DIR/target" "$NEXT_DIR/node_modules"

View File

@@ -0,0 +1,4 @@
#!/bin/sh
driver_name='errors-json'
git config merge."$driver_name".name 'Automatically merge packages/next/errors.json'
git config merge."$driver_name".driver 'node scripts/merge-errors-json/merge.mjs %A %O %B'

View File

@@ -0,0 +1,121 @@
// @ts-check
/**
* Git merge driver for packages/next/errors.json
*
* This script automatically resolves merge conflicts in the auto-generated
* errors.json file by reassigning error codes to avoid conflicts.
*
* Usage: node merge-errors-json.mjs <current> <base> <other> [<marker-size>]
*
* Arguments:
* - current: Path to the current version (our changes)
* - base: Path to the common ancestor version
* - other: Path to the other version (their changes)
* - marker-size: Size of conflict markers (optional, defaults to 7)
*
* Exit codes:
* - 0: Merge successful, result written to current file
* - 1: Merge failed, conflicts remain
*/
import { readFileSync, writeFileSync } from 'node:fs'
function main() {
const args = process.argv.slice(2)
if (args.length < 3) {
console.error(
'Usage: node merge-errors-json.mjs <current> <base> <other> [<marker-size>]'
)
process.exit(1)
}
const [currentPath, basePath, otherPath] = args
try {
const base = readJsonSync(basePath)
const current = readJsonSync(currentPath)
const other = readJsonSync(otherPath)
const merged = mergeErrors(base, current, other)
// Git expects the result to be written to the "current" file
writeJsonSync(currentPath, merged)
const addedCount = Object.keys(merged).length - Object.keys(current).length
console.error(
`merge-errors-json: added ${addedCount === 1 ? '1 new message' : `${addedCount} new messages`} to errors.json`
)
process.exit(0)
} catch (error) {
console.error('merge-errors-json: merge failed:', error.message)
console.error()
console.error(
[
'if this error persists, you can disable the merge driver by running',
'',
' scripts/merge-errors-json/uninstall',
'',
'or by manually removing the `[merge "errors-json"]` section from your .git/config.',
].join('\n')
)
process.exit(1)
}
}
/**
* @typedef {Record<string, string>} ErrorsMap
*/
/**
* Merge three versions of errors.json, resolving conflicts by assigning new sequential IDs
* @param {ErrorsMap} base - Base version (common ancestor)
* @param {ErrorsMap} current - Current version (our changes, or the state of the branch we're rebasing onto)
* @param {ErrorsMap} other - Other version (their changes, or the state the branch we're rebasing, a.k.a. "incoming")
* @returns {ErrorsMap}
*/
function mergeErrors(base, current, other) {
/** @type {ErrorsMap} */
const result = { ...current }
/** @type {Set<string>} */
const existingMessages = new Set(Object.values(result))
let nextKey = Object.keys(result).length + 1
for (const message of getNewMessages(base, other)) {
if (existingMessages.has(message)) {
continue
}
const key = nextKey++
result[key] = message
existingMessages.add(message)
}
return result
}
function getNewMessages(
/** @type {ErrorsMap} */ prev,
/** @type {ErrorsMap} */ current
) {
const existing = new Set(Object.values(prev))
return Object.values(current).filter((msg) => !existing.has(msg))
}
function readJsonSync(/** @type {string} */ filePath) {
const content = readFileSync(filePath, 'utf8')
return JSON.parse(content)
}
function writeJsonSync(
/** @type {string} */ filePath,
/** @type {any} */ value
) {
const content = JSON.stringify(value, null, 2) + '\n'
writeFileSync(filePath, content, 'utf8')
}
main()

View File

@@ -0,0 +1,3 @@
#!/bin/sh
driver_name='errors-json'
git config --remove-section merge."$driver_name"

197
scripts/minimal-server.js Normal file
View File

@@ -0,0 +1,197 @@
console.time('next-wall-time')
// Usage: node scripts/minimal-server.js <path-to-app-dir-build> <path-to-page>
// This script is used to run a minimal Next.js server in production mode.
process.env.NODE_ENV = 'production'
// Change this to 'experimental' to opt into the React experimental channel (needed for server actions, ppr)
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = 'next'
let currentNode = null
let outliers = []
const chalk = {
yellow: (str) => `\x1b[33m${str}\x1b[0m`,
green: (str) => `\x1b[32m${str}\x1b[0m`,
}
if (process.env.LOG_REQUIRE) {
const originalCompile = require('module').prototype._compile
require('module').prototype._compile = function (_content, filename) {
let parent = currentNode
currentNode = {
id: filename,
selfDuration: 0,
totalDuration: 0,
children: [],
}
const start = performance.now()
const result = originalCompile.apply(this, arguments)
const end = performance.now()
currentNode.totalDuration = end - start
currentNode.selfDuration = currentNode.children.reduce(
(acc, child) => acc - child.selfDuration,
currentNode.totalDuration
)
parent?.children.push(currentNode)
currentNode = parent || currentNode
return result
}
}
function prettyPrint(
node,
distDir,
prefix = '',
isLast = false,
isRoot = true
) {
let duration = `${node.selfDuration.toFixed(
2
)}ms / ${node.totalDuration.toFixed(2)}ms`
if (node.selfDuration > 70) {
duration = chalk.yellow(duration)
outliers.push(node)
}
let output = `${prefix}${isLast || isRoot ? '└─ ' : '├─ '}${chalk.green(
path.relative(distDir, node.id)
)} ${chalk.yellow(duration)}\n`
const childPrefix = `${prefix}${isRoot ? ' ' : isLast ? ' ' : '│ '}`
node.children.forEach((child, i) => {
output += prettyPrint(
child,
node.id,
childPrefix,
i === node.children.length - 1,
false
)
})
return output
}
if (process.env.LOG_COMPILE) {
const originalCompile = require('module').prototype._compile
const currentDir = process.cwd()
require('module').prototype._compile = function (content, filename) {
const strippedFilename = filename.replace(currentDir, '')
console.time(`Module '${strippedFilename}' compiled`)
const result = originalCompile.apply(this, arguments)
console.timeEnd(`Module '${strippedFilename}' compiled`)
return result
}
}
const appDir = process.argv[2]
const absoluteAppDir = require('path').resolve(appDir)
process.chdir(absoluteAppDir)
let readFileCount = 0
let readFileSyncCount = 0
if (process.env.LOG_READFILE) {
const originalReadFile = require('fs').readFile
const originalReadFileSync = require('fs').readFileSync
require('fs').readFile = function (path, options, callback) {
readFileCount++
console.log(`readFile: ${require('path').relative(absoluteAppDir, path)}`)
return originalReadFile.apply(this, arguments)
}
require('fs').readFileSync = function (path, options) {
readFileSyncCount++
console.log(
`readFileSync: ${require('path').relative(absoluteAppDir, path)}`
)
return originalReadFileSync.apply(this, arguments)
}
}
console.time('next-cold-start')
const NextServer = process.env.USE_BUNDLED_NEXT
? require('next/dist/compiled/next-server/server.runtime.prod').default
: require('next/dist/server/next-server').default
if (process.env.LOG_READFILE) {
console.log(`readFileCount: ${readFileCount + readFileSyncCount}`)
}
const path = require('path')
const distDir = '.next'
const compiledConfig = require(
path.join(absoluteAppDir, distDir, 'required-server-files.json')
).config
const nextServer = new NextServer({
conf: compiledConfig,
dir: '.',
distDir: distDir,
minimalMode: true,
customServer: false,
})
const requestHandler = nextServer.getRequestHandler()
require('http')
.createServer((req, res) => {
console.time('next-request')
readFileCount = 0
readFileSyncCount = 0
return requestHandler(req, res)
.catch((err) => {
console.error(err)
res.statusCode = 500
res.end('Internal Server Error')
})
.finally(() => {
console.timeEnd('next-request')
if (process.env.LOG_READFILE) {
console.log(`readFileCount: ${readFileCount + readFileSyncCount}`)
}
})
})
.listen(3000, () => {
console.timeEnd('next-cold-start')
fetch('http://localhost:3000/' + (process.argv[3] || ''))
.then((res) => res.text())
.catch((err) => {
console.error(err)
})
.finally(() => {
console.timeEnd('next-wall-time')
if (process.env.LOG_REQUIRE) {
console.log(
prettyPrint(currentNode, path.join(absoluteAppDir, distDir))
)
if (outliers.length > 0) {
console.log('Outliers:')
outliers.forEach((node) => {
console.log(
` ${path.relative(
path.join(absoluteAppDir, distDir),
node.id
)} ${node.selfDuration.toFixed(
2
)}ms / ${node.totalDuration.toFixed(2)}ms`
)
})
}
}
require('process').exit(0)
})
})

39
scripts/next-with-deps.sh Normal file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
START_DIR=$PWD
# gets last argument which should be the project dir
for PROJECT_DIR in $@;do :;done
if [ -z $PROJECT_DIR ];then
echo "No project directory provided, exiting..."
exit 1;
fi;
if [ ! -d $PROJECT_DIR ];then
echo "Invalid project directory provided, exiting..."
exit 1;
fi;
if [ $PROJECT_DIR == $PWD ] || [ "$PROJECT_DIR" == "." ];then
echo "Project directory can not be root, exiting..."
exit 1;
fi;
CONFLICTING_DEPS=("react" "react-dom" "styled-jsx" "next")
for dep in ${CONFLICTING_DEPS[@]};do
if [ -d "$PROJECT_DIR/node_modules/$dep" ];then
HAS_CONFLICTING_DEP="yup"
fi;
done
if [ ! -z $HAS_CONFLICTING_DEP ] || [ ! -d "$PROJECT_DIR/node_modules" ];then
cd $PROJECT_DIR
pnpm i --ignore-workspace --no-lockfile
for dep in ${CONFLICTING_DEPS[@]};do
rm -rf node_modules/$dep
done
fi
cd $START_DIR
pnpm next $@

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env node
// @ts-check
/*
This prevents busting the turbo cache un-necessarily due
to bumping the version in the repo's package.json files
*/
const path = require('path')
const fs = require('fs/promises')
const cwd = process.cwd()
const NORMALIZED_VERSION = '0.0.0'
const readJson = async (filePath) =>
JSON.parse(await fs.readFile(filePath, 'utf8'))
const writeJson = async (filePath, data) =>
fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n')
;(async function () {
const packages = await fs.readdir(path.join(cwd, 'packages'))
const pkgJsonData = new Map()
const pkgNames = []
await Promise.all(
packages.map(async (pkgDir) => {
const data = await readJson(
path.join(cwd, 'packages', pkgDir, 'package.json')
)
pkgNames.push(data.name)
pkgJsonData.set(pkgDir, data)
})
)
const normalizeVersions = async (filePath, data) => {
data = data || (await readJson(filePath))
const version = data.version
if (version) {
data.version = NORMALIZED_VERSION
const normalizeEntry = (type, key) => {
const pkgVersion = data[type][key]
if (pkgNames.includes(key) && pkgVersion === version) {
data[type][key] = NORMALIZED_VERSION
}
}
for (const key of Object.keys(data.dependencies || {})) {
normalizeEntry('dependencies', key)
}
for (const key of Object.keys(data.devDependencies || {})) {
normalizeEntry('devDependencies', key)
}
await writeJson(filePath, data)
}
}
await Promise.all(
packages.map((pkgDir) =>
normalizeVersions(
path.join(cwd, 'packages', pkgDir, 'package.json'),
pkgJsonData.get(pkgDir)
)
)
)
await normalizeVersions(path.join(cwd, 'lerna.json'))
await fs.unlink(path.join(cwd, 'pnpm-lock.yaml'))
await fs.writeFile(path.join(cwd, 'pnpm-lock.yaml'), '')
const rootPkgJsonPath = path.join(cwd, 'package.json')
await writeJson(rootPkgJsonPath, {
name: 'nextjs-project',
version: '0.0.0',
private: true,
workspaces: ['packages/*'],
scripts: {},
packageManager: 'pnpm@9.6.0',
})
})()

258
scripts/pack-next.ts Normal file
View File

@@ -0,0 +1,258 @@
// This script must be run with tsx
import fs from 'node:fs/promises'
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import { default as patchPackageJson } from './pack-utils/patch-package-json.js'
import buildNative from './build-native.js'
import {
NEXT_DIR,
exec,
execAsyncWithOutput,
glob,
packageFiles,
} from './pack-util.js'
const TARBALLS = `${NEXT_DIR}/tarballs`
const NEXT_PACKAGES = `${NEXT_DIR}/packages`
const NEXT_TARBALL = `${TARBALLS}/next.tar`
const NEXT_SWC_TARBALL = `${TARBALLS}/next-swc.tar`
const NEXT_MDX_TARBALL = `${TARBALLS}/next-mdx.tar`
const NEXT_ENV_TARBALL = `${TARBALLS}/next-env.tar`
const NEXT_BA_TARBALL = `${TARBALLS}/next-bundle-analyzer.tar`
type CompressOpt = 'none' | 'strip' | 'objcopy-zlib' | 'objcopy-zstd'
const cliOptions = yargs(hideBin(process.argv))
.scriptName('pack-next')
.command('$0', 'Pack Next.js for testing in external projects')
.option('js-build', {
type: 'boolean',
default: true,
describe:
'Build JavaScript code (default). Use `--no-js-build` to skip building JavaScript',
})
.option('project', {
alias: 'p',
type: 'string',
})
.option('tar', {
type: 'boolean',
describe: 'Create tarballs instead of direct reflinks',
})
.option('compress', {
describe:
'How compress the binary, useful on platforms where tarballs can ' +
'exceed 2 GiB, which causes ERR_FS_FILE_TOO_LARGE with pnpm. Defaults ' +
'to "strip" on Linux, otherwise defaults to "none". Requires `--tar` ' +
'to be set.',
choices: [
'none',
'strip',
...(process.platform === 'linux'
? (['objcopy-zlib', 'objcopy-zstd'] as const)
: ([] as const)),
] as const,
})
.check((opts) => {
const compress = opts.compress
if (!opts.tar && (compress ?? 'none') !== 'none') {
throw new Error('--compress is only valid in combination with --tar')
}
return true
})
.middleware((opts) => {
if (opts.tar && process.platform === 'linux' && opts.compress == null) {
opts.compress = 'strip'
}
})
.strict().argv
interface PackageFiles {
nextFile: string
nextMdxFile: string
nextEnvFile: string
nextBaFile: string
nextSwcFile: string
}
async function main(): Promise<void> {
if (cliOptions.jsBuild) {
exec('Install Next.js build dependencies', 'pnpm i')
exec('Build Next.js', 'pnpm run build')
}
if (cliOptions.tar && cliOptions.compress !== 'strip') {
// HACK: delete any pre-existing binaries to force napi-rs to rewrite it
// We must do this as pre-existing could've been stripped.
let binaries = await nextSwcBinaries()
await Promise.all(binaries.map((bin) => fs.rm(bin)))
}
await buildNative(cliOptions._ as string[])
if (cliOptions.tar) {
await fs.mkdir(TARBALLS, { recursive: true })
// build all tarfiles in parallel
await Promise.all([
packNextSwcWithTar(cliOptions.compress ?? 'none'),
...[
[`${NEXT_PACKAGES}/next`, NEXT_TARBALL],
[`${NEXT_PACKAGES}/next-mdx`, NEXT_MDX_TARBALL],
[`${NEXT_PACKAGES}/next-env`, NEXT_ENV_TARBALL],
[`${NEXT_PACKAGES}/next-bundle-analyzer`, NEXT_BA_TARBALL],
].map(([packagePath, tarballPath]) =>
packWithTar(packagePath, tarballPath)
),
])
}
const packageFiles = getPackageFiles(cliOptions.tar)
if (cliOptions.project != null) {
const patchedPath = await patchPackageJson(cliOptions.project, {
nextTarball: packageFiles.nextFile,
nextMdxTarball: packageFiles.nextMdxFile,
nextEnvTarball: packageFiles.nextEnvFile,
nextBundleAnalyzerTarball: packageFiles.nextBaFile,
nextSwcTarball: packageFiles.nextSwcFile,
})
console.log(`Patched ${patchedPath}`)
} else {
console.log('Add the following overrides to your workspace package.json:')
console.log(` "pnpm": {`)
console.log(` "overrides": {`)
console.log(
` "next": ${JSON.stringify(`file:${packageFiles.nextFile}`)},`
)
console.log(
` "@next/mdx": ${JSON.stringify(`file:${packageFiles.nextMdxFile}`)},`
)
console.log(
` "@next/env": ${JSON.stringify(`file:${packageFiles.nextEnvFile}`)},`
)
console.log(
` "@next/bundle-analyzer": ${JSON.stringify(`file:${packageFiles.nextBaFile}`)}`
)
console.log(` }`)
console.log(` }`)
console.log()
console.log(
'Add the following dependencies to your workspace package.json:'
)
console.log(` "dependencies": {`)
console.log(
` "@next/swc": ${JSON.stringify(`file:${packageFiles.nextSwcFile}`)},`
)
console.log(` ...`)
console.log(` }`)
console.log()
}
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
async function nextSwcBinaries(): Promise<string[]> {
return await glob('next-swc/native/*.node', {
cwd: NEXT_PACKAGES,
absolute: true,
})
}
// We use neither:
// * npm pack, as it doesn't include native modules in the tarball
// * pnpm pack, as it tries to include target directories and compress them,
// which takes forever.
// Instead, we generate non-compressed tarballs.
async function packWithTar(
packagePath: string,
tarballPath: string,
extraArgs: string[] = []
): Promise<void> {
const paths = await packageFiles(packagePath)
const command = [
'tar',
'-c',
// https://apple.stackexchange.com/a/444073
...(process.platform === 'darwin' ? ['--no-mac-metadata'] : []),
'-f',
tarballPath,
...extraArgs,
'--',
...paths.map((p) => `./${p}`),
]
await execAsyncWithOutput(`Pack ${packagePath}`, command, {
cwd: packagePath,
})
}
// Special-case logic for packing next-swc.
//
// pnpm emits `ERR_FS_FILE_TOO_LARGE` if the tarfile is >2GiB due to limits
// in libuv (https://github.com/libuv/libuv/pull/1501). This is common with
// next-swc due to the large amount of debugging symbols. We can fix this one
// of two ways: strip or compression.
//
// We default to stripping (usually faster), but on Linux, we can compress
// instead with objcopy, keeping debug symbols intact. This is controlled by
// `PACK_NEXT_COMPRESS`.
async function packNextSwcWithTar(compress: CompressOpt): Promise<void> {
const packagePath = `${NEXT_PACKAGES}/next-swc`
switch (compress) {
case 'strip':
await execAsyncWithOutput('Stripping next-swc native binary', [
'strip',
...(process.platform === 'darwin' ? ['-x', '-'] : ['--']),
...(await nextSwcBinaries()),
])
await packWithTar(packagePath, NEXT_SWC_TARBALL)
break
case 'objcopy-zstd':
case 'objcopy-zlib':
// Linux-specific, feature is gated by yargs choices array
const format = compress === 'objcopy-zstd' ? 'zstd' : 'zlib'
await Promise.all(
(await nextSwcBinaries()).map((bin) =>
execAsyncWithOutput(
'Compressing debug symbols in next-swc native binary',
['objcopy', `--compress-debug-sections=${format}`, '--', bin]
)
)
)
await packWithTar(packagePath, NEXT_SWC_TARBALL)
break
case 'none':
await packWithTar(packagePath, NEXT_SWC_TARBALL)
break
default:
// should never happen, yargs enforces the `choices` array
throw new Error('compress value is invalid')
}
}
function getPackageFiles(shouldCreateTarballs?: boolean): PackageFiles {
if (shouldCreateTarballs) {
return {
nextFile: NEXT_TARBALL,
nextMdxFile: NEXT_MDX_TARBALL,
nextEnvFile: NEXT_ENV_TARBALL,
nextBaFile: NEXT_BA_TARBALL,
nextSwcFile: NEXT_SWC_TARBALL,
}
}
return {
nextFile: `${NEXT_PACKAGES}/next`,
nextMdxFile: `${NEXT_PACKAGES}/next-mdx`,
nextEnvFile: `${NEXT_PACKAGES}/next-env`,
nextBaFile: `${NEXT_PACKAGES}/next-bundle-analyzer`,
nextSwcFile: `${NEXT_PACKAGES}/next-swc`,
}
}

178
scripts/pack-util.ts Normal file
View File

@@ -0,0 +1,178 @@
import {
execSync,
execFileSync,
spawn,
ExecSyncOptionsWithStringEncoding,
} from 'child_process'
import { existsSync } from 'fs'
import globOrig from 'glob'
import { join } from 'path'
import { promisify } from 'util'
export const glob = promisify(globOrig)
export const NEXT_DIR = join(__dirname, '..')
/**
* @param {string} title
* @param {string | string[]} command
* @param {ExecSyncOptions} [opts]
* @returns {string}
*/
export function exec(title, command, opts?: ExecSyncOptionsWithStringEncoding) {
if (Array.isArray(command)) {
logCommand(title, command)
return execFileSync(command[0], command.slice(1), {
stdio: 'inherit',
cwd: NEXT_DIR,
...opts,
})
} else {
logCommand(title, command)
return execSync(command, {
stdio: 'inherit',
cwd: NEXT_DIR,
...opts,
})
}
}
class ExecError extends Error {
code: number | null
stdout: Buffer
stderr: Buffer
}
type ExecOutput = {
stdout: Buffer
stderr: Buffer
}
/**
* @param {string} title
* @param {string | string[]} command
* @param {SpawnOptions} [opts]
*/
export function execAsyncWithOutput(
title,
command,
opts?: Partial<ExecSyncOptionsWithStringEncoding>
): Promise<ExecOutput> {
logCommand(title, command)
const proc = spawn(command[0], command.slice(1), {
encoding: 'utf8',
stdio: ['inherit', 'pipe', 'pipe'],
cwd: NEXT_DIR,
...opts,
})
if (!proc || !proc.stdout || !proc.stderr) {
throw new Error(`Failed to spawn: ${title}`)
}
const stdout: Buffer[] = []
proc.stdout.on('data', (data) => {
process.stdout.write(data)
stdout.push(data)
})
const stderr: Buffer[] = []
proc.stderr.on('data', (data) => {
process.stderr.write(data)
stderr.push(data)
})
return new Promise((resolve, reject) => {
proc.on('exit', (code) => {
if (code === 0) {
return resolve({
stdout: Buffer.concat(stdout),
stderr: Buffer.concat(stderr),
})
}
const err = new ExecError(
`Command failed with exit code ${code}: ${prettyCommand(command)}`
)
err.code = code
err.stdout = Buffer.concat(stdout)
err.stderr = Buffer.concat(stderr)
reject(err)
})
})
}
/**
* @template T
* @param {string} title
* @param {() => T} fn
* @returns {T}
*/
export function execFn<T>(title: string, fn: () => T): T {
logCommand(title, fn.toString())
return fn()
}
/**
* @param {string | string[]} command
*/
function prettyCommand(command: string | string[]): string {
if (Array.isArray(command)) command = command.join(' ')
return command.replace(/ -- .*/, ' -- …')
}
/**
* @param {string} title
* @param {string | string[]} [command]
*/
export function logCommand(title: string, command: string | string[]) {
if (command) {
const pretty = prettyCommand(command)
console.log(`\n\x1b[1;4m${title}\x1b[0m\n> \x1b[1m${pretty}\x1b[0m\n`)
} else {
console.log(`\n\x1b[1;4m${title}\x1b[0m\n`)
}
}
const DEFAULT_GLOBS = ['**', '!target', '!node_modules', '!crates', '!.turbo']
const FORCED_GLOBS = ['package.json', 'README*', 'LICENSE*', 'LICENCE*']
/**
* @param {string} path
* @returns {Promise<string[]>}
*/
export async function packageFiles(path: string): Promise<string[]> {
const { files = DEFAULT_GLOBS, main, bin } = require(`${path}/package.json`)
const allFiles: string[] = files.concat(
FORCED_GLOBS,
main ?? [],
Object.values(bin ?? {})
)
const isGlob = (f) => f.includes('*') || f.startsWith('!')
const simpleFiles = allFiles
.filter((f) => !isGlob(f) && existsSync(join(path, f)))
.map((f) => f.replace(/^\.\//, ''))
const globFiles = allFiles.filter(isGlob)
const globbedFiles = await glob(
`+(${globFiles.filter((f) => !f.startsWith('!')).join('|')})`,
{
cwd: path,
ignore: `+(${globFiles
.filter((f) => f.startsWith('!'))
.map((f) => f.slice(1))
.join('|')})`,
}
)
const packageFiles = [...globbedFiles, ...simpleFiles].sort()
const set = new Set()
return packageFiles.filter((f) => {
if (set.has(f)) return false
// We add the full path, but check for parent directories too.
// This catches the case where the whole directory is added and then a single file from the directory.
// The sorting before ensures that the directory comes before the files inside of the directory.
while (f.includes('/')) {
f = f.replace(/\/[^/]*$/, '')
if (set.has(f)) return false
}
set.add(f)
return true
})
}

View File

@@ -0,0 +1,210 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import os from 'node:os'
export interface DependencyPaths {
nextTarball: string
nextMdxTarball: string
nextEnvTarball: string
nextBundleAnalyzerTarball: string
nextSwcTarball: string
}
interface NextPeerDeps {
react: string
reactDom: string
[key: string]: unknown
}
export interface PackageJson {
dependencies?: Record<string, string>
peerDependencies?: Record<string, string>
overrides?: Record<string, string>
resolutions?: Record<string, string>
workspaces?: string[] | { packages: string[] }
[key: string]: unknown
}
/**
* Main function to patch a project's package.json with Next.js tarball references
* @param paths Configuration options for patching
* @returns The path to the patched file
*/
export default async function patchPackageJson(
targetProjectPath: string,
paths: DependencyPaths
): Promise<string> {
try {
const root = await findWorkspaceRoot(targetProjectPath)
const packageJsonPath = root
? path.join(root, 'package.json')
: path.join(targetProjectPath, 'package.json')
const packageJsonValue = await readJsonValue(packageJsonPath)
await patchWorkspacePackageJsonMap(paths, packageJsonValue)
await writeJsonValue(packageJsonPath, packageJsonValue)
return packageJsonPath
} catch (error) {
throw new Error('Error patching package.json', { cause: error })
}
}
async function readJsonValue(filePath: string): Promise<PackageJson> {
try {
const content = await fs.readFile(filePath, 'utf8')
return JSON.parse(content) as PackageJson
} catch (error) {
throw new Error(`Could not read or parse ${filePath}`, { cause: error })
}
}
async function writeJsonValue(
filePath: string,
value: PackageJson
): Promise<void> {
try {
const content = JSON.stringify(value, null, 2) + os.EOL
await fs.writeFile(filePath, content)
} catch (error) {
throw new Error(`Failed to write ${filePath}`, { cause: error })
}
}
async function patchWorkspacePackageJsonMap(
paths: DependencyPaths,
packageJsonMap: PackageJson
): Promise<PackageJson> {
const nextPeerDeps = await getNextPeerDeps()
const overrides: [string, string][] = [
['next', `file:${paths.nextTarball}`],
['@next/mdx', `file:${paths.nextMdxTarball}`],
['@next/env', `file:${paths.nextEnvTarball}`],
['@next/bundle-analyzer', `file:${paths.nextBundleAnalyzerTarball}`],
['@next/swc', `file:${paths.nextSwcTarball}`],
['react', nextPeerDeps.react],
['react-dom', nextPeerDeps.reactDom],
]
// npm uses `overrides`
packageJsonMap.overrides = packageJsonMap.overrides || {}
insertMapEntries(packageJsonMap.overrides, overrides)
// yarn uses `resolutions`
packageJsonMap.resolutions = packageJsonMap.resolutions || {}
insertMapEntries(packageJsonMap.resolutions, overrides)
// Add @next/swc to dependencies
packageJsonMap.dependencies = packageJsonMap.dependencies || {}
insertMapEntries(packageJsonMap.dependencies, [
['@next/swc', `file:${paths.nextSwcTarball}`],
])
// Update direct dependencies to match overrides
updateMapEntriesIfExists(packageJsonMap.dependencies, overrides)
return packageJsonMap
}
/**
* Get Next.js peer dependencies from its package.json
*/
async function getNextPeerDeps(): Promise<NextPeerDeps> {
try {
// Navigate to the next package.json relative to the current module
const currentFilePath = fileURLToPath(import.meta.url)
const scriptDir = path.dirname(currentFilePath)
const packageJsonPath = path.resolve(
scriptDir,
'../../packages/next/package.json'
)
const content = await fs.readFile(packageJsonPath, 'utf8')
const nextPackageJson = JSON.parse(content) as PackageJson
if (!nextPackageJson.peerDependencies) {
throw new Error('Next.js package.json is missing peerDependencies')
}
return {
react: nextPackageJson.peerDependencies.react || '',
reactDom: nextPackageJson.peerDependencies['react-dom'] || '',
}
} catch (error) {
throw new Error('Failed to get Next.js peer dependencies', { cause: error })
}
}
function insertMapEntries(
map: Record<string, string>,
entries: [string, string][]
): void {
for (const [key, value] of entries) {
map[key] = value
}
}
function updateMapEntriesIfExists(
map: Record<string, string>,
entries: [string, string][]
): void {
for (const [key, value] of entries) {
if (map[key] !== undefined) {
map[key] = value
}
}
}
async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}
/**
* @returns null if not in a workspace
*/
async function findWorkspaceRoot(projectPath: string): Promise<string | null> {
// Check environment variables
const envVars = ['NPM_CONFIG_WORKSPACE_DIR', 'npm_config_workspace_dir']
for (const ev of envVars) {
if (process.env[ev]) {
return process.env[ev]
}
}
try {
const canonicalPath = await fs.realpath(projectPath)
let currentDir = canonicalPath
// Walk up the directory tree
while (currentDir !== path.parse(currentDir).root) {
// Check for pnpm workspace
if (await fileExists(path.join(currentDir, 'pnpm-workspace.yaml'))) {
return currentDir
}
// Check for npm/yarn workspace
const packageJsonPath = path.join(currentDir, 'package.json')
if (await fileExists(packageJsonPath)) {
const packageJson = await readJsonValue(packageJsonPath)
if (packageJson.workspaces) {
return currentDir
}
}
// Move up to parent directory
currentDir = path.dirname(currentDir)
}
// No workspace found
return null
} catch (error) {
throw new Error('Failed to find workspace root', { cause: error })
}
}

176
scripts/patch-next.ts Normal file
View File

@@ -0,0 +1,176 @@
// the script must be run with tsx
import fs from 'fs'
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import path from 'path'
import { NEXT_DIR, exec, execFn, packageFiles } from './pack-util.js'
import buildNative from './build-native.js'
interface Options {
project: string
build: boolean
'build-native': boolean
verbose: number
_: string[]
}
// --- Parse command line arguments ---
const argv = yargs(hideBin(process.argv))
.scriptName('patch-next')
.command(
'$0 <project> [options]',
'Patch local Next.js packages to the target project directory',
(yargs) => {
return yargs
.positional('project', {
type: 'string',
describe: 'Target directory of the Next.js project to patch',
demandOption: true,
})
.example(
'$0 ../my-app --no-build --no-build-native',
'Patch Next.js packages in the "my-app" directory'
)
.example(
'$0 ../my-app -- --release',
'Patch using a release-mode native build. `--release` is passed through to the napi CLI'
)
}
)
.option('build', {
type: 'boolean',
default: true,
description: 'Run the Next.js build step (`pnpm i` and `pnpm build`).',
})
.option('build-native', {
alias: 'native-build',
type: 'boolean',
default: true,
description: 'Run the native modules build step.',
})
.option('verbose', {
type: 'number',
choices: [0, 1, 2, 3],
count: true,
alias: 'v',
description: 'Set the verbosity level (-v: WARN, -vv: INFO, -vvv: DEBUG)',
})
.wrap(null)
.help()
.alias('help', 'h')
.demandCommand(1, 'A project directory is required.')
.strictCommands()
.parse()
const {
project: projectDir,
build,
'build-native': buildNativeEnabled,
verbose: verboseLevel,
_: buildNativeArgs,
} = argv as Options
function WARN(...args: any[]) {
verboseLevel >= 1 && console.warn(...args)
}
function INFO(...args: any[]) {
verboseLevel >= 2 && console.info(...args)
}
function DEBUG(...args: any[]) {
verboseLevel >= 3 && console.log(...args)
}
const PROJECT_DIR = path.resolve(projectDir)
const NEXT_PACKAGES = path.join(NEXT_DIR, 'packages')
function realPathIfAny(path: string): string | null {
try {
return fs.realpathSync(path)
} catch {
return null
}
}
async function copy(src: string, dst: string): Promise<void> {
const realDst = realPathIfAny(dst)
if (!realDst) {
WARN(`[x] Destination path ${dst} does not exist. Skipping copy.`)
return
}
if (realDst && realDst === src) {
WARN(
`[x] Source and destination paths are the same: ${src}. Skipping copy.`
)
return
}
if (!fs.existsSync(src)) {
WARN(`[x] Source path ${src} does not exist. Skipping copy.`)
return
}
const files = await packageFiles(src)
DEBUG(`[x] Found ${files.length} files to copy from ${src}`)
for (const file of files) {
const srcFile = path.join(src, file)
const dstFile = path.join(realDst, file)
DEBUG(`Copying ${srcFile} to ${dstFile}`)
fs.cpSync(srcFile, dstFile, {
recursive: true,
})
}
}
// --- Main execution ---
async function main(): Promise<void> {
if (!fs.existsSync(PROJECT_DIR)) {
console.error(`Error: Project directory "${PROJECT_DIR}" does not exist.`)
process.exit(1)
}
INFO(`[x] Project Directory: ${PROJECT_DIR}`)
INFO(`[x] Next.js Source: ${NEXT_PACKAGES}`)
if (build) {
exec('Install Next.js build dependencies', 'pnpm i')
exec('Build Next.js', 'pnpm run build')
}
if (buildNativeEnabled) {
INFO('Building native modules...')
await buildNative(buildNativeArgs)
}
const packagesToPatch = [
{ name: 'next', path: 'next' },
{ name: '@next/swc', path: 'next-swc' },
{ name: '@next/mdx', path: 'next-mdx' },
{ name: '@next/bundle-analyzer', path: 'next-bundle-analyzer' },
]
INFO(
`[x] Patching packages: ${packagesToPatch.map((pkg) => pkg.name).join(', ')}`
)
for (const pkg of packagesToPatch) {
await execFn(`Patching ${pkg.name}`, () =>
copy(
path.join(NEXT_PACKAGES, pkg.path),
path.join(PROJECT_DIR, 'node_modules', pkg.name)
)
)
}
console.log(`\n\x1b[1;4mPatching complete!\x1b[0m\n`)
}
main().catch((e) => {
console.error('An unexpected error occurred:')
console.error(e)
process.exit(1)
})

View File

@@ -0,0 +1,233 @@
import { execSync } from 'node:child_process'
import fs from 'node:fs/promises'
import path from 'node:path'
import { parseArgs } from 'node:util'
const BASE_URL = 'https://vercel-packages.vercel.app/next/commits'
const PACKAGES_TO_PATCH = [
'next',
'@next/mdx',
'@next/env',
'@next/bundle-analyzer',
]
// --- Argument parsing ---
function parseAndValidateArgs() {
const { values } = parseArgs({
options: {
project: { type: 'string' },
commit: { type: 'string' },
branch: { type: 'string' },
},
strict: true,
})
if (!values.project) {
console.error(
'Usage: node scripts/patch-preview-tarball.mjs --project <path> [--commit <sha> | --branch <name>]'
)
process.exit(1)
}
if (values.commit && values.branch) {
console.error('Error: --commit and --branch are mutually exclusive.')
process.exit(1)
}
return {
project: path.resolve(values.project),
commit: values.commit,
branch: values.branch,
}
}
// --- Resolve commit SHA ---
function resolveCommitSha({ commit, branch }) {
if (commit) {
if (!/^[0-9a-f]{7,40}$/i.test(commit)) {
console.error(`Error: Invalid commit SHA: ${commit}`)
process.exit(1)
}
return commit
}
if (branch) {
const encoded = encodeURIComponent(branch)
try {
const sha = execSync(
`gh api "repos/vercel/next.js/branches/${encoded}" --jq '.commit.sha'`,
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
).trim()
if (!sha) {
console.error(
`Error: Could not resolve branch '${branch}' to a commit SHA.`
)
process.exit(1)
}
return sha
} catch (err) {
console.error(
`Error: Failed to look up branch '${branch}' via GitHub API.`
)
console.error(err.stderr?.toString() || err.message)
process.exit(1)
}
}
// Fallback: local HEAD
try {
return execSync('git rev-parse HEAD', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
}).trim()
} catch (err) {
console.error('Error: Failed to resolve local HEAD commit.')
console.error(err.stderr?.toString() || err.message)
process.exit(1)
}
}
// --- URL construction ---
function buildTarballUrls(commitSha) {
const urls = new Map()
for (const pkg of PACKAGES_TO_PATCH) {
urls.set(pkg, `${BASE_URL}/${commitSha}/${pkg}`)
}
return urls
}
// --- Tarball verification ---
async function verifyTarballExists(url) {
try {
const res = await fetch(url, { method: 'HEAD', redirect: 'follow' })
return res.ok
} catch {
return false
}
}
// --- Workspace root finding ---
async function fileExists(filePath) {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}
async function findWorkspaceRoot(projectPath) {
for (const ev of ['NPM_CONFIG_WORKSPACE_DIR', 'npm_config_workspace_dir']) {
if (process.env[ev]) {
return process.env[ev]
}
}
try {
const canonicalPath = await fs.realpath(projectPath)
let currentDir = canonicalPath
while (currentDir !== path.parse(currentDir).root) {
if (await fileExists(path.join(currentDir, 'pnpm-workspace.yaml'))) {
return currentDir
}
const packageJsonPath = path.join(currentDir, 'package.json')
if (await fileExists(packageJsonPath)) {
const content = await fs.readFile(packageJsonPath, 'utf8')
const pkg = JSON.parse(content)
if (pkg.workspaces) {
return currentDir
}
}
currentDir = path.dirname(currentDir)
}
return null
} catch {
return null
}
}
// --- Patch package.json ---
async function patchPackageJson(projectPath, tarballUrls) {
const root = await findWorkspaceRoot(projectPath)
const packageJsonPath = root
? path.join(root, 'package.json')
: path.join(projectPath, 'package.json')
if (!(await fileExists(packageJsonPath))) {
console.error(`Error: package.json not found at ${packageJsonPath}`)
process.exit(1)
}
const content = await fs.readFile(packageJsonPath, 'utf8')
const pkg = JSON.parse(content)
const entries = Array.from(tarballUrls.entries())
// npm/pnpm overrides
pkg.overrides = pkg.overrides || {}
for (const [name, url] of entries) {
pkg.overrides[name] = url
}
// yarn resolutions
pkg.resolutions = pkg.resolutions || {}
for (const [name, url] of entries) {
pkg.resolutions[name] = url
}
await fs.writeFile(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n')
console.log(`Patched ${packageJsonPath}`)
console.log('Packages overridden:')
for (const [name, url] of entries) {
console.log(` ${name} -> ${url}`)
}
return packageJsonPath
}
// --- Main ---
async function main() {
const { project, commit, branch } = parseAndValidateArgs()
const sha = resolveCommitSha({ commit, branch })
console.log(`Resolved commit SHA: ${sha}`)
const tarballUrls = buildTarballUrls(sha)
const nextUrl = tarballUrls.get('next')
console.log(`Verifying preview tarball exists: ${nextUrl}`)
const exists = await verifyTarballExists(nextUrl)
if (!exists) {
console.error(
`Preview tarball not found for commit ${sha}.\n` +
`The "Deploy preview tarball" job may not have completed yet, or the commit may not have a build.\n` +
`Check: https://github.com/vercel/next.js/actions/workflows/build_and_deploy.yml`
)
process.exit(1)
}
console.log('Preview tarball verified.')
await patchPackageJson(project, tarballUrls)
console.log(
'\nDone! Run your package manager install command to apply changes.'
)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

1529
scripts/pr-status.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,299 @@
#!/usr/bin/env node
/**
* Next.js CPU Profile Script
*
* Generates CPU profiles for Next.js startup and dev server boot.
*
* Usage:
* node scripts/profile-next-dev-boot.js [options]
*
* Options:
* --test-dir=PATH Test project directory (default: /private/tmp/next-boot-test)
* --output-dir=PATH Output directory for profiles (default: ./profiles)
* --turbopack Use Turbopack (default)
* --webpack Use Webpack
* --duration=MS How long to profile after ready (default: 1000)
* --cli Profile just the CLI entry point (runs next --help)
*
* Output files:
* - dev-turbopack-YYYY-MM-DDTHH-MM-SS.cpuprofile
* - cli-turbopack-YYYY-MM-DDTHH-MM-SS.cpuprofile
*
* The profile can be loaded in:
* - Chrome DevTools (Performance tab -> Load profile)
* - VS Code (JavaScript Profile Visualizer extension)
* - https://www.speedscope.app/
*
* Note: Currently profiles the parent process only. For child process profiling,
* additional Next.js changes are needed (see future PRs).
*/
const { spawn, execSync } = require('child_process')
const path = require('path')
const fs = require('fs')
// Parse arguments
const args = process.argv.slice(2)
const getArg = (name, defaultValue) => {
const arg = args.find((a) => a.startsWith(`--${name}=`))
return arg ? arg.split('=')[1] : defaultValue
}
const hasFlag = (name) => args.includes(`--${name}`)
const testDir = getArg('test-dir', '/private/tmp/next-boot-test')
const baseOutputDir =
getArg('output-dir', null) || path.join(process.cwd(), 'profiles')
const useWebpack = hasFlag('webpack')
const duration = parseInt(getArg('duration', '1000'), 10)
const profileCli = hasFlag('cli')
const bundlerFlag = useWebpack ? '--webpack' : '--turbopack'
// Generate meaningful profile names
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
const bundlerName = useWebpack ? 'webpack' : 'turbopack'
const profileType = profileCli ? 'cli' : 'dev'
const outputDir = baseOutputDir
const profileName = `${profileType}-${bundlerName}-${timestamp}`
const nextDir = path.join(__dirname, '..', 'packages', 'next')
const nextBin = path.join(nextDir, 'dist/bin/next')
if (profileCli) {
console.log('\x1b[34m=== Next.js CLI Entry Point Profile ===\x1b[0m')
} else {
console.log('\x1b[34m=== Next.js Dev Server CPU Profile ===\x1b[0m')
console.log(`Test directory: ${testDir}`)
console.log(`Bundler: ${useWebpack ? 'Webpack' : 'Turbopack'}`)
}
console.log(`Output directory: ${outputDir}`)
console.log('')
// Verify test directory (only for dev server profiling)
if (!profileCli && !fs.existsSync(testDir)) {
console.error(
`\x1b[31mError: Test directory does not exist: ${testDir}\x1b[0m`
)
process.exit(1)
}
// Create output directory
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
// Kill existing processes
function killNextDev() {
try {
execSync('pkill -f "next dev"', { stdio: 'ignore' })
} catch {}
}
async function runProfile() {
killNextDev()
await new Promise((r) => setTimeout(r, 500))
// Clean .next directory
const nextCache = path.join(testDir, '.next')
if (fs.existsSync(nextCache)) {
fs.rmSync(nextCache, { recursive: true, force: true })
}
console.log('Starting dev server with CPU profiling...')
console.log('(Profile will be saved after server is ready)')
console.log('')
return new Promise((resolve, reject) => {
let resolved = false
// Profile the parent process with --cpu-prof
const spawnArgs = [
process.execPath,
[
'--cpu-prof',
`--cpu-prof-dir=${outputDir}`,
`--cpu-prof-name=${profileName}`,
nextBin,
'dev',
bundlerFlag,
],
]
const child = spawn(spawnArgs[0], spawnArgs[1], {
cwd: testDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, FORCE_COLOR: '0' },
})
let output = ''
const onData = (data) => {
const text = data.toString()
output += text
process.stdout.write(text)
// Wait for "Ready in Xms"
if (output.includes('Ready in') && !resolved) {
resolved = true
console.log('')
console.log(
`\x1b[33mServer ready, profiling for ${duration}ms more...\x1b[0m`
)
// Wait a bit then stop
setTimeout(() => {
console.log('Stopping server and saving profile...')
child.kill('SIGINT')
}, duration)
}
}
child.stdout.on('data', onData)
child.stderr.on('data', onData)
child.on('close', (code) => {
killNextDev()
// Wait a moment for profile files to be written
setTimeout(() => {
// Find and rename profiles matching our name pattern
// --cpu-prof-name creates files without extension
const files = fs.readdirSync(outputDir)
const rawFiles = files.filter(
(f) => f.startsWith(profileName) && !f.endsWith('.cpuprofile')
)
// Rename raw files to have .cpuprofile extension
rawFiles.forEach((f) => {
const oldPath = path.join(outputDir, f)
const newPath = path.join(outputDir, `${f}.cpuprofile`)
fs.renameSync(oldPath, newPath)
})
// Now find all .cpuprofile files
const profileFiles = fs
.readdirSync(outputDir)
.filter((f) => f.startsWith(profileName) && f.endsWith('.cpuprofile'))
const profiles = profileFiles
.map((f) => ({
name: f,
path: path.join(outputDir, f),
size: fs.statSync(path.join(outputDir, f)).size,
}))
.filter((p) => p.size > 0)
.sort((a, b) => b.size - a.size)
if (profiles.length > 0) {
console.log('')
console.log(`\x1b[32mProfile(s) saved:\x1b[0m`)
profiles.forEach((p, i) => {
const sizeKB = Math.round(p.size / 1024)
console.log(` ${i + 1}. ${p.path} (${sizeKB} KB)`)
})
console.log('')
console.log('To view the profile:')
console.log(' 1. Open Chrome DevTools -> Performance tab')
console.log(' 2. Click "Load profile" and select the file')
console.log(' 3. Or use https://www.speedscope.app/')
console.log('')
console.log(
'\x1b[33mTip:\x1b[0m The largest profile is usually the child process (server worker)'
)
resolve(profiles[0].path)
} else {
console.log('')
console.log(
'\x1b[33mNo profiles found. Trying alternative method...\x1b[0m'
)
console.log('')
console.log(
'To profile the child process, modify next-dev.ts to add profiling flags.'
)
console.log(
'Or use: node --cpu-prof --cpu-prof-dir=./profiles ./dist/bin/next dev'
)
reject(new Error('Profile file not found'))
}
}, 500)
})
child.on('error', reject)
// Timeout
setTimeout(() => {
if (!resolved) {
child.kill('SIGKILL')
reject(new Error('Timeout waiting for server'))
}
}, 120000)
})
}
async function runCliProfile() {
console.log('Profiling CLI entry point (next --help)...')
console.log('')
return new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
[
'--cpu-prof',
`--cpu-prof-dir=${outputDir}`,
`--cpu-prof-name=${profileName}`,
nextBin,
'--help',
],
{
cwd: process.cwd(),
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, FORCE_COLOR: '0' },
}
)
child.stdout.on('data', () => {})
child.stderr.on('data', () => {})
child.on('close', (code) => {
// Wait for profile to be written
setTimeout(() => {
// --cpu-prof-name creates files without extension, rename to .cpuprofile
const rawFile = path.join(outputDir, profileName)
const finalFile = path.join(outputDir, `${profileName}.cpuprofile`)
if (fs.existsSync(rawFile)) {
fs.renameSync(rawFile, finalFile)
const size = fs.statSync(finalFile).size
console.log(`\x1b[32mProfile saved:\x1b[0m`)
console.log(` ${finalFile} (${Math.round(size / 1024)} KB)`)
console.log('')
console.log('To view the profile:')
console.log(' 1. Open Chrome DevTools -> Performance tab')
console.log(' 2. Click "Load profile" and select the file')
console.log(' 3. Or use https://www.speedscope.app/')
console.log('')
console.log(
'\x1b[33mTip:\x1b[0m Look for heavy modules loaded at startup'
)
resolve(finalFile)
} else if (fs.existsSync(finalFile)) {
const size = fs.statSync(finalFile).size
console.log(`\x1b[32mProfile saved:\x1b[0m`)
console.log(` ${finalFile} (${Math.round(size / 1024)} KB)`)
resolve(finalFile)
} else {
reject(new Error('Profile file not found'))
}
}, 500)
})
child.on('error', reject)
})
}
// Main execution
const main = profileCli ? runCliProfile : runProfile
main().catch((err) => {
console.error('\x1b[31mError:\x1b[0m', err.message)
if (!profileCli) killNextDev()
process.exit(1)
})

148
scripts/publish-native.js Normal file
View File

@@ -0,0 +1,148 @@
#!/usr/bin/env node
const path = require('path')
const execa = require('execa')
const { Sema } = require('async-sema')
const { readFile, readdir, writeFile, cp } = require('fs/promises')
const cwd = process.cwd()
;(async function () {
try {
const publishSema = new Sema(2)
let version = require('@next/swc/package.json').version
// Copy binaries to package folders, update version, and publish
let nativePackagesDir = path.join(cwd, 'crates/next-napi-bindings/npm')
let platforms = (await readdir(nativePackagesDir)).filter(
(name) => !name.startsWith('.')
)
await Promise.all(
platforms.map(async (platform) => {
await publishSema.acquire()
let output = ''
try {
let binaryName = `next-swc.${platform}.node`
await cp(
path.join(cwd, 'packages/next-swc/native', binaryName),
path.join(nativePackagesDir, platform, binaryName)
)
let pkg = JSON.parse(
await readFile(
path.join(nativePackagesDir, platform, 'package.json')
)
)
pkg.version = version
await writeFile(
path.join(nativePackagesDir, platform, 'package.json'),
JSON.stringify(pkg, null, 2)
)
const child = execa(
`npm`,
[
`publish`,
`${path.join(nativePackagesDir, platform)}`,
`--access`,
`public`,
...(version.includes('canary') ? ['--tag', 'canary'] : []),
],
{ stdio: 'inherit' }
)
const handleData = (type) => (chunk) => {
process[type].write(chunk)
output += chunk.toString()
}
child.stdout?.on('data', handleData('stdout'))
child.stderr?.on('data', handleData('stderr'))
await child
} catch (err) {
// don't block publishing other versions on single platform error
console.error(`Failed to publish`, platform, err)
if (
output.includes(
'cannot publish over the previously published versions'
)
) {
console.error('Ignoring already published error', platform, err)
} else {
// throw err
}
} finally {
publishSema.release()
}
})
)
// Update name/version of wasm packages and publish
const pkgDirectory = 'crates/wasm'
let wasmDir = path.join(cwd, pkgDirectory)
await Promise.all(
['web', 'nodejs'].map(async (wasmTarget) => {
await publishSema.acquire()
let wasmPkg = JSON.parse(
await readFile(path.join(wasmDir, `pkg-${wasmTarget}/package.json`))
)
wasmPkg.name = `@next/swc-wasm-${wasmTarget}`
wasmPkg.version = version
wasmPkg.repository = {
type: 'git',
url: 'https://github.com/vercel/next.js',
directory: pkgDirectory,
}
await writeFile(
path.join(wasmDir, `pkg-${wasmTarget}/package.json`),
JSON.stringify(wasmPkg, null, 2)
)
try {
await execa(
`npm`,
[
'publish',
`${path.join(wasmDir, `pkg-${wasmTarget}`)}`,
'--access',
'public',
...(version.includes('canary') ? ['--tag', 'canary'] : []),
],
{ stdio: 'inherit' }
)
} catch (err) {
// don't block publishing other versions on single platform error
console.error(`Failed to publish`, wasmTarget, err)
if (
err.message &&
err.message.includes(
'You cannot publish over the previously published versions'
)
) {
console.error('Ignoring already published error', wasmTarget)
} else {
// throw err
}
} finally {
publishSema.release()
}
})
)
// Update optional dependencies versions
let nextPkg = JSON.parse(
await readFile(path.join(cwd, 'packages/next/package.json'))
)
for (let platform of platforms) {
let optionalDependencies = nextPkg.optionalDependencies || {}
optionalDependencies['@next/swc-' + platform] = version
nextPkg.optionalDependencies = optionalDependencies
}
await writeFile(
path.join(path.join(cwd, 'packages/next/package.json')),
JSON.stringify(nextPkg, null, 2)
)
} catch (err) {
console.error(err)
process.exit(1)
}
})()

224
scripts/publish-release.js Normal file
View File

@@ -0,0 +1,224 @@
#!/usr/bin/env node
// @ts-check
const path = require('path')
const execa = require('execa')
const semver = require('semver')
const { Sema } = require('async-sema')
const { execSync } = require('child_process')
const fs = require('fs')
const cwd = process.cwd()
;(async function () {
let isCanary = true
let isReleaseCandidate = false
let isBeta = false
try {
const tagOutput = execSync(
`node ${path.join(__dirname, 'check-is-release.js')}`
).toString()
console.log(tagOutput)
if (tagOutput.trim().startsWith('v')) {
isCanary = tagOutput.includes('-canary')
}
isReleaseCandidate = tagOutput.includes('-rc')
isBeta = tagOutput.includes('-beta')
} catch (err) {
console.log(err)
if (err.message && err.message.includes('no tag exactly matches')) {
console.log('Nothing to publish, exiting...')
return
}
throw err
}
let tag = isCanary
? 'canary'
: isReleaseCandidate
? 'rc'
: isBeta
? 'beta'
: 'latest'
try {
if (!isCanary && !isReleaseCandidate && !isBeta) {
const version = JSON.parse(
await fs.promises.readFile(path.join(cwd, 'lerna.json'), 'utf-8')
).version
const res = await fetch(
`https://registry.npmjs.org/-/package/next/dist-tags`
)
const tags = await res.json()
if (semver.lt(version, tags.latest)) {
// If the current version is less than the latest, it means this
// is a backport release. Since NPM sets the 'latest' tag by default
// during publishing, when users install `next@latest`, they might
// get the backported version instead of the actual "latest" version.
// Therefore, we explicitly set the tag as 'backport' for backports.
tag = 'backport'
}
}
} catch (error) {
console.log('Failed to fetch Next.js dist tags from the NPM registry.')
throw error
}
console.log(`Publishing as "${tag}" dist tag...`)
if (!process.env.NPM_TOKEN) {
console.log('No NPM_TOKEN, exiting...')
return
}
const packagesDir = path.join(cwd, 'packages')
const packageDirs = fs.readdirSync(packagesDir)
const publishSema = new Sema(2)
const publish = async (pkg, retry = 0) => {
let output = ''
try {
await publishSema.acquire()
const child = execa(
`npm`,
[
'publish',
`${path.join(packagesDir, pkg)}`,
'--access',
'public',
'--ignore-scripts',
'--tag',
tag,
],
{ stdio: 'pipe' }
)
const handleData = (type) => (chunk) => {
process[type].write(chunk)
output += chunk.toString()
}
child.stdout?.on('data', handleData('stdout'))
child.stderr?.on('data', handleData('stderr'))
// Return here to avoid retry logic
return await child
} catch (err) {
console.error(`Failed to publish ${pkg}`, err)
if (
output.includes('cannot publish over the previously published versions')
) {
console.error('Ignoring already published error', pkg)
return
}
if (retry >= 3) {
throw err
}
} finally {
publishSema.release()
}
// Recursive call need to be outside of the publishSema
const retryDelaySeconds = 15
console.log(`retrying in ${retryDelaySeconds}s`)
await new Promise((resolve) =>
setTimeout(resolve, retryDelaySeconds * 1000)
)
await publish(pkg, retry + 1)
}
const undraft = async () => {
const githubToken = process.env.RELEASE_BOT_GITHUB_TOKEN
if (!githubToken) {
throw new Error(`Missing RELEASE_BOT_GITHUB_TOKEN`)
}
if (isCanary) {
try {
const ghHeaders = {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${githubToken}`,
'X-GitHub-Api-Version': '2022-11-28',
}
const { version: _version } = require('../lerna.json')
const version = `v${_version}`
let release
let releasesData
// The release might take a minute to show up in
// the list so retry a bit
for (let i = 0; i < 6; i++) {
try {
const releaseUrlRes = await fetch(
`https://api.github.com/repos/vercel/next.js/releases`,
{
headers: ghHeaders,
}
)
releasesData = await releaseUrlRes.json()
release = releasesData.find(
(release) => release.tag_name === version
)
} catch (err) {
console.log(`Fetching release failed`, err)
}
if (!release) {
console.log(`Retrying in 10s...`)
await new Promise((resolve) => setTimeout(resolve, 10 * 1000))
}
}
if (!release) {
console.log(`Failed to find release`, releasesData)
return
}
const undraftRes = await fetch(release.url, {
headers: ghHeaders,
method: 'PATCH',
body: JSON.stringify({
draft: false,
name: version,
}),
})
if (undraftRes.ok) {
console.log('un-drafted canary release successfully')
} else {
console.log(`Failed to undraft`, await undraftRes.text())
}
} catch (err) {
console.error(`Failed to undraft release`, err)
}
}
}
const results = await Promise.allSettled(
packageDirs.map(async (packageDir) => {
const pkgJson = JSON.parse(
await fs.promises.readFile(
path.join(packagesDir, packageDir, 'package.json'),
'utf-8'
)
)
if (pkgJson.private) {
console.log(`Skipping private package ${packageDir}`)
return
}
await publish(packageDir)
})
)
if (results.some((item) => item.status === 'rejected')) {
console.error(`Not all packages published successfully`, results)
process.exit(1)
}
await undraft()
})()

126
scripts/pull-turbo-cache.js Normal file
View File

@@ -0,0 +1,126 @@
#!/usr/bin/env node
// @ts-check
const { spawn } = require('child_process')
const MAX_ATTEMPTS = 3
const RETRY_DELAY_MS = 5000
/**
* @param {number} ms
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* @param {string} command
* @param {{ stdio?: 'pipe' | 'inherit', captureOutput?: boolean }} options
* @returns {Promise<{ code: number | null, signal: string | null, output: string }>}
*/
function runCommand(
command,
{ stdio = 'inherit', captureOutput = false } = {}
) {
return new Promise((resolve) => {
let output = ''
const child = spawn('/bin/bash', ['-c', command], {
stdio: captureOutput ? 'pipe' : stdio,
})
if (captureOutput) {
child.stdout?.on('data', (data) => {
process.stdout.write(data)
output += data.toString()
})
child.stderr?.on('data', (data) => {
process.stderr.write(data)
})
}
child.on('exit', (code, signal) => {
resolve({ code, signal, output })
})
})
}
/**
* @param {string} command
* @param {number} maxAttempts
* @returns {Promise<boolean>}
*/
async function runWithRetry(command, maxAttempts) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
console.log(`Attempt ${attempt}/${maxAttempts}...`)
const { code, signal } = await runCommand(command)
if (!code && !signal) {
return true // success
}
console.warn(
`Attempt ${attempt} failed (exit code ${code}, signal ${signal})`
)
if (attempt < maxAttempts) {
console.log(`Retrying in ${RETRY_DELAY_MS / 1000}s...`)
await sleep(RETRY_DELAY_MS)
}
}
return false // all attempts failed
}
;(async function () {
const target = process.argv[process.argv.length - 1]
const turboCommand = `pnpm dlx turbo@${process.env.TURBO_VERSION || 'latest'}`
// First, do a dry run to check cache status
const { code, signal, output } = await runCommand(
`${turboCommand} run cache-build-native --dry=json -- ${target}`,
{ captureOutput: true }
)
if (code || signal) {
console.warn(
`Dry run failed (exit code ${code}, signal ${signal}). Continuing without cache.`
)
return
}
let turboData
try {
turboData = JSON.parse(output)
} catch (e) {
console.warn(`Failed to parse turbo output: ${e.message}`)
return
}
const task = turboData.tasks.find((t) => t.command !== '<NONEXISTENT>')
if (!task) {
console.warn(`Failed to find related turbo task`, output)
return
}
// Pull cache if it was available
if (task.cache.local || task.cache.remote) {
console.log('Cache Status', task.taskId, task.hash, task.cache)
const success = await runWithRetry(
`${turboCommand} run cache-build-native -- ${target}`,
MAX_ATTEMPTS
)
if (!success) {
// Don't fail the job - the workflow will check if build exists
// and build from source if needed
console.warn(
`Cache restoration failed after ${MAX_ATTEMPTS} attempts. ` +
`Build will proceed from source.`
)
}
} else {
console.warn(`No turbo cache was available, continuing...`)
console.warn(task)
}
})()

18
scripts/release-stats.sh Normal file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
if [[ $(node ./scripts/check-is-release.js 2> /dev/null || :) == v* ]];
then
echo "Publish occurred, running release stats..."
else
echo "Not publish commit, exiting..."
touch .github/actions/next-stats-action/SKIP_NEXT_STATS.txt
exit 0;
fi
if [[ -z "$NPM_TOKEN" ]];then
echo "No NPM_TOKEN, exiting.."
exit 0;
fi
echo "Waiting 30 seconds to allow publish to finalize"
sleep 30

148
scripts/reset-project.mjs Normal file
View File

@@ -0,0 +1,148 @@
import fetch from 'node-fetch'
export const TEST_PROJECT_NAME = 'vtest314-e2e-tests'
export const TEST_TEAM_NAME = process.env.VERCEL_TEST_TEAM
export const TEST_TOKEN = process.env.VERCEL_TEST_TOKEN
export const ADAPTER_TEST_TEAM_NAME = process.env.VERCEL_ADAPTER_TEST_TEAM
export const ADAPTER_TEST_TOKEN = process.env.VERCEL_ADAPTER_TEST_TOKEN
export const TURBOPACK_TEST_TEAM_NAME = process.env.VERCEL_TURBOPACK_TEST_TEAM
export const TURBOPACK_TEST_TOKEN = process.env.VERCEL_TURBOPACK_TEST_TOKEN
/**
* Retry a fetch request with exponential backoff
* @param {string} url - The URL to fetch
* @param {object} options - Fetch options
* @param {object} config - Retry configuration
* @param {number} config.maxRetries - Maximum number of retry attempts (default: 5)
* @param {number[]} config.acceptableStatuses - Status codes that are acceptable and should not retry (default: [])
* @param {string} config.operationName - Name of the operation for logging (default: 'Request')
* @returns {Promise<Response>} The fetch response
*/
async function fetchWithRetry(
url,
options = {},
{ maxRetries = 5, acceptableStatuses = [], operationName = 'Request' } = {}
) {
let lastError
let response
for (let attempt = 0; attempt < maxRetries; attempt++) {
response = await fetch(url, options)
// Check if response is acceptable
if (response.ok || acceptableStatuses.includes(response.status)) {
return response
}
// If we have attempts remaining, retry
if (attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000 // exponential backoff: 1s, 2s, 4s, 8s, 16s
const errorText = await response.text()
console.log(
`${operationName} failed with status ${response.status} (attempt ${attempt + 1}/${maxRetries}), waiting ${delay}ms before retrying...`
)
lastError = `${operationName} failed. Got status: ${response.status}, ${errorText}`
await new Promise((resolve) => setTimeout(resolve, delay))
continue
}
// Last attempt failed, capture error
lastError = `${operationName} failed. Got status: ${
response.status
}, ${await response.text()}`
}
// All retries exhausted
throw new Error(lastError)
}
export async function resetProject({
teamId = TEST_TEAM_NAME,
projectName = TEST_PROJECT_NAME,
token = TEST_TOKEN,
disableDeploymentProtection = true,
}) {
console.log(`Resetting project ${teamId}/${projectName}`)
// TODO: error/bail if existing deployments are pending
await fetchWithRetry(
`https://vercel.com/api/v8/projects/${encodeURIComponent(
projectName
)}?teamId=${teamId}`,
{
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
},
{
acceptableStatuses: [404], // 404 is acceptable (project doesn't exist)
operationName: 'Delete project',
}
)
// Retry logic for project creation since deletion may be async
const createRes = await fetchWithRetry(
`https://vercel.com/api/v8/projects?teamId=${teamId}`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: projectName,
framework: 'nextjs',
resourceConfig: {
buildMachineType: 'enhanced',
},
environmentVariables: [
{
key: 'VERCEL_FORCE_NO_BUILD_CACHE_UPLOAD',
value: '1',
type: 'plain',
target: ['production', 'preview', 'development'],
},
],
}),
},
{
operationName: 'Create project',
}
)
const { id: projectId } = await createRes.json()
if (!projectId) {
throw new Error("Couldn't get projectId from create project response")
}
if (disableDeploymentProtection) {
console.log('Disabling deployment protection...')
await fetchWithRetry(
`https://vercel.com/api/v8/projects/${encodeURIComponent(
projectId
)}?teamId=${teamId}`,
{
method: 'PATCH',
headers: {
'content-type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
ssoProtection: null,
passwordProtection: null,
}),
},
{
operationName: 'Disable deployment protection',
}
)
}
console.log(
`Successfully created fresh Vercel project ${teamId}/${projectName}`
)
}

13
scripts/rm.mjs Normal file
View File

@@ -0,0 +1,13 @@
// @ts-check
import { rm } from 'fs/promises'
import { join } from 'path'
const args = process.argv.slice(2)
if (args.length === 0) {
throw new Error('rm.mjs: requires a least one parameter')
}
for (const arg of args) {
const path = join(process.cwd(), arg)
console.log(`rm.mjs: deleting path "${path}"`)
await rm(path, { recursive: true, force: true })
}

View File

@@ -0,0 +1,41 @@
import {
resetProject,
TEST_PROJECT_NAME,
TEST_TEAM_NAME,
ADAPTER_TEST_TEAM_NAME,
ADAPTER_TEST_TOKEN,
TURBOPACK_TEST_TEAM_NAME,
TURBOPACK_TEST_TOKEN,
TEST_TOKEN,
} from './reset-project.mjs'
async function main() {
let hadFailure = false
for (const { teamId, token } of [
{ teamId: TEST_TEAM_NAME, token: TEST_TOKEN },
{ teamId: ADAPTER_TEST_TEAM_NAME, token: ADAPTER_TEST_TOKEN },
{ teamId: TURBOPACK_TEST_TEAM_NAME, token: TURBOPACK_TEST_TOKEN },
]) {
try {
await resetProject({
projectName: TEST_PROJECT_NAME,
teamId,
token,
disableDeploymentProtection: true,
})
} catch (err) {
console.error(err)
hadFailure = true
}
}
if (hadFailure) {
throw new Error(`resetting a project failed`)
}
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

181
scripts/run-for-change.mjs Normal file
View File

@@ -0,0 +1,181 @@
// @ts-check
import { promisify } from 'util'
import { exec as execOrig, spawn } from 'child_process'
import { getDiffRevision, getGitInfo } from './git-info.mjs'
const exec = promisify(execOrig)
const CHANGE_ITEM_GROUPS = {
docs: [
'bench',
'docs',
'apps/docs',
'errors',
'examples',
'UPGRADING.md',
'contributing.md',
'contributing',
'CODE_OF_CONDUCT.md',
'readme.md',
'.github/ISSUE_TEMPLATE',
'.github/labeler.json',
'.github/pull_request_template.md',
'packages/next-plugin-storybook/readme.md',
'packages/next/license.md',
'packages/next/README.md',
'packages/eslint-plugin-next/README.md',
'packages/next-codemod/license.md',
'packages/next-codemod/README.md',
'crates/wasm/README.md',
'packages/next-swc/README.md',
'packages/next-bundle-analyzer/readme.md',
'packages/next-mdx/license.md',
'packages/next-mdx/readme.md',
'packages/react-dev-overlay/README.md',
'packages/react-refresh-utils/README.md',
'packages/create-next-app/README.md',
'packages/font/README.md',
'packages/next-env/README.md',
'packages/next/src/client/components/react-dev-overlay/README.md',
],
'deploy-examples': ['examples/image-component'],
cna: [
'packages/create-next-app',
'test/integration/create-next-app',
'examples/basic-css',
'examples/mdx-pages',
'examples/with-sass',
'examples/with-eslint',
],
'next-codemod': ['packages/next-codemod'],
'next-swc': [
'packages/next-swc',
'scripts/normalize-version-bump.js',
'test/integration/create-next-app',
'scripts/send-trace-to-jaeger',
],
}
async function main() {
const { branchName, remoteUrl, isCanary } = await getGitInfo()
const diffRevision = await getDiffRevision()
const changesResult = await exec(
`git diff ${diffRevision} --name-only`
).catch((err) => {
console.error(err)
return { stdout: '' }
})
console.error({ branchName, remoteUrl, isCanary, changesResult })
const changedFilesOutput = changesResult.stdout
const typeIndex = process.argv.indexOf('--type')
const type = typeIndex > -1 && process.argv[typeIndex + 1]
const isNegated = process.argv.indexOf('--not') > -1
const alwaysCanary = process.argv.indexOf('--always-canary') > -1
if (!type) {
throw new Error(
`Missing "--type" flag, e.g. "node run-for-change.mjs --type docs"`
)
}
const execArgIndex = process.argv.indexOf('--exec')
const listChangedDirectories = process.argv.includes(
'--listChangedDirectories'
)
if (execArgIndex < 0 && !listChangedDirectories) {
throw new Error(
'Invalid: must provide either "--exec" or "--listChangedDirectories" flag'
)
}
let hasMatchingChange = false
const changeItems = CHANGE_ITEM_GROUPS[type]
const execArgs = process.argv.slice(execArgIndex + 1)
if (execArgs.length < 1 && !listChangedDirectories) {
throw new Error('Missing exec arguments after "--exec"')
}
if (!changeItems) {
throw new Error(
`Invalid change type, allowed types are ${Object.keys(
CHANGE_ITEM_GROUPS
).join(', ')}`
)
}
let changedFilesCount = 0
let changedDirectories = new Set()
// always run for canary if flag is enabled
if (alwaysCanary && branchName === 'canary') {
changedFilesCount += 1
hasMatchingChange = true
}
for (let file of changedFilesOutput.split('\n')) {
file = file.trim().replace(/\\/g, '/')
if (file) {
changedFilesCount += 1
// if --not flag is provided we execute for any file changed
// not included in the change items otherwise we only execute
// if a change item is changed
const matchesItem = changeItems.some((item) => {
const found = file.startsWith(item)
if (found) {
changedDirectories.add(item)
}
return found
})
if (!matchesItem && isNegated) {
hasMatchingChange = true
break
}
if (matchesItem && !isNegated) {
hasMatchingChange = true
break
}
}
}
// if we fail to detect the changes run the command
if (changedFilesCount < 1) {
console.error(`No changed files detected:\n${changedFilesOutput}`)
hasMatchingChange = true
}
if (hasMatchingChange) {
if (listChangedDirectories) {
console.log(Array.from(changedDirectories).join('\n'))
return
}
const cmd = spawn(execArgs[0], execArgs.slice(1))
cmd.stdout.pipe(process.stdout)
cmd.stderr.pipe(process.stderr)
await new Promise((resolve, reject) => {
cmd.on('exit', (code) => {
if (code !== 0) {
return reject(new Error('command failed with code: ' + code))
}
resolve()
})
cmd.on('error', (err) => reject(err))
})
} else if (!listChangedDirectories) {
console.log(
`No matching changed files for ${isNegated ? 'not ' : ''}"${type}":\n` +
changedFilesOutput.trim()
)
}
}
main().catch((err) => {
console.error('Failed to detect changes', err)
process.exit(1)
})

952
scripts/send-trace-to-jaeger/Cargo.lock generated Normal file
View File

@@ -0,0 +1,952 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bumpalo"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]]
name = "cc"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "core-foundation"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "encoding_rs"
version = "0.8.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
dependencies = [
"cfg-if",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
"matches",
"percent-encoding",
]
[[package]]
name = "futures-channel"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
[[package]]
name = "futures-io"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
[[package]]
name = "futures-sink"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
[[package]]
name = "futures-task"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
[[package]]
name = "futures-util"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
dependencies = [
"autocfg",
"futures-core",
"futures-io",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "h2"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "http"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
dependencies = [
"bytes",
"http",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
[[package]]
name = "httpdate"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
[[package]]
name = "hyper"
version = "0.14.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "ipnet"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "js-sys"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mio"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
dependencies = [
"libc",
"log",
"miow",
"ntapi",
"winapi",
]
[[package]]
name = "miow"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
"winapi",
]
[[package]]
name = "native-tls"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
dependencies = [
"winapi",
]
[[package]]
name = "num_cpus"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "once_cell"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "openssl"
version = "0.10.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-sys",
]
[[package]]
name = "openssl-probe"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
[[package]]
name = "openssl-sys"
version = "0.9.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6517987b3f8226b5da3661dad65ff7f300cc59fb5ea8333ca191fc65fde3edf"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project-lite"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f"
[[package]]
name = "ppv-lite86"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
[[package]]
name = "proc-macro2"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
dependencies = [
"rand_core",
]
[[package]]
name = "redox_syscall"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [
"bitflags",
]
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "reqwest"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66d2927ca2f685faf0fc620ac4834690d29e7abb153add10f5812eef20b5e280"
dependencies = [
"base64",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"http",
"http-body",
"hyper",
"hyper-tls",
"ipnet",
"js-sys",
"lazy_static",
"log",
"mime",
"native-tls",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
]
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "schannel"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
dependencies = [
"lazy_static",
"winapi",
]
[[package]]
name = "security-framework"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "send-trace-to-jaeger"
version = "0.1.0"
dependencies = [
"reqwest",
"serde_json",
]
[[package]]
name = "serde"
version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
[[package]]
name = "serde_json"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "slab"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]]
name = "socket2"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "syn"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "tempfile"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
"cfg-if",
"libc",
"rand",
"redox_syscall",
"remove_dir_all",
"winapi",
]
[[package]]
name = "tinyvec"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
dependencies = [
"autocfg",
"bytes",
"libc",
"memchr",
"mio",
"num_cpus",
"pin-project-lite",
"winapi",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"log",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tower-service"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
[[package]]
name = "tracing"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
dependencies = [
"cfg-if",
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
dependencies = [
"lazy_static",
]
[[package]]
name = "try-lock"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "unicode-bidi"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
[[package]]
name = "unicode-normalization"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "url"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"percent-encoding",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "want"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
dependencies = [
"log",
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasm-bindgen"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
[[package]]
name = "web-sys"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "winreg"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
dependencies = [
"winapi",
]

View File

@@ -0,0 +1,14 @@
[package]
name = "send-trace-to-jaeger"
version = "0.1.0"
edition = "2024"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lints]
workspace = true
[dependencies]
serde_json = { workspace = true }
reqwest = { workspace = true, features = ["blocking"] }

View File

@@ -0,0 +1,112 @@
use std::{
env::args,
fs::File,
io::{self, BufRead},
path::Path,
};
use reqwest::blocking::Client;
use serde_json::{Map, Number, Value};
/// Read individual lines from a file.
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where
P: AsRef<Path>,
{
let file = File::open(filename)?;
Ok(io::BufReader::new(file).lines())
}
/// Log the url to view the trace in the browser.
fn log_web_url(jaeger_web_ui_url: &str, trace_id: &str) {
println!("Jaeger trace will be available on {jaeger_web_ui_url}/trace/{trace_id}")
}
/// Send trace JSON to Jaeger using ZipKin API.
fn send_json_to_zipkin(zipkin_api: &str, value: String) {
let client = Client::new();
let res = client
.post(zipkin_api)
.header("Content-Type", "application/json")
.body(value)
.send()
.expect("Failed to send request");
if !res.status().is_success() {
println!("body = {:?}", res.text());
}
}
// function to append zero to a number until 16 characters
fn pad_zeros(num: u64) -> String {
let mut num_str = num.to_string();
while num_str.len() < 16 {
num_str = format!("0{num_str}");
}
num_str
}
fn main() {
let service_name = "nextjs";
let ipv4 = "127.0.0.1";
let port = 9411;
let zipkin_url = format!("http://{ipv4}:{port}");
let jaeger_web_ui_url = format!("http://{ipv4}:16686");
let zipkin_api = format!("{zipkin_url}/api/v2/spans");
let mut logged_url = false;
let mut local_endpoint = Map::new();
local_endpoint.insert(
"serviceName".to_string(),
Value::String(service_name.to_string()),
);
local_endpoint.insert("ipv4".to_string(), Value::String(ipv4.to_string()));
local_endpoint.insert("port".to_string(), Value::Number(Number::from(port)));
let first_arg = args().nth(1).expect("Please provide a file name");
if let Ok(lines) = read_lines(first_arg) {
for json_to_parse in lines.map_while(Result::ok) {
let v = match serde_json::from_str::<Vec<Value>>(&json_to_parse) {
Ok(v) => v
.into_iter()
.map(|mut data| {
if !logged_url {
log_web_url(&jaeger_web_ui_url, data["traceId"].as_str().unwrap());
logged_url = true;
}
data["localEndpoint"] = Value::Object(local_endpoint.clone());
data["id"] = Value::String(pad_zeros(data["id"].as_u64().unwrap()));
if data["parentId"] != Value::Null {
data["parentId"] =
Value::String(pad_zeros(data["parentId"].as_u64().unwrap()));
}
if let Some(tags) = data["tags"].as_object_mut() {
for (_, value) in tags.iter_mut() {
if value.is_boolean() {
let bool_val = value.as_bool().unwrap();
*value = serde_json::Value::String(bool_val.to_string());
}
}
}
data
})
.collect::<Value>(),
Err(e) => {
println!("{e}");
continue;
}
};
let json_map = serde_json::to_string(&v).expect("Failed to serialize");
// println!("{:}", json_map);
send_json_to_zipkin(&zipkin_api, json_map);
}
}
}

View File

@@ -0,0 +1,56 @@
// @ts-check
const execa = require('execa')
const fs = require('node:fs/promises')
const path = require('node:path')
async function main() {
const [githubSha] = process.argv.slice(2)
if (!githubSha) {
throw new Error('Usage: set-preview-version.js <githubSha>')
}
const repoRoot = path.resolve(__dirname, '..')
const [{ stdout: shortSha }, { stdout: dateString }] = await Promise.all([
execa('git', ['rev-parse', '--short', githubSha]),
// Source: https://github.com/facebook/react/blob/767f52237cf7892ad07726f21e3e8bacfc8af839/scripts/release/utils.js#L114
execa('git', [
'show',
'-s',
'--no-show-signature',
'--format=%cd',
'--date=format:%Y%m%d',
githubSha,
]),
])
const lernaConfigPath = path.join(repoRoot, 'lerna.json')
const lernaConfig = JSON.parse(await fs.readFile(lernaConfigPath, 'utf8'))
// 15.0.0-canary.17 -> 15.0.0
// 15.0.0 -> 15.0.0
const [semverStableVersion] = lernaConfig.version.split('-')
const version = `${semverStableVersion}-preview-${shortSha}-${dateString}`
await execa(
'pnpm',
[
'lerna',
'version',
version,
'--no-git-tag-version',
'--no-push',
'--allow-branch',
'**',
'--yes',
],
{ cwd: repoRoot, stdio: 'inherit' }
)
console.info(`Set preview version: ${version}`)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

6
scripts/setup-node.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
# retry setting up Node.js version 5 times waiting 15 seconds between
for i in 1 2 3 4 5;
do curl -s https://install-node.vercel.app/v${NODE_VERSION} | FORCE=1 bash && break || sleep 15;
done

93
scripts/start-release.js Normal file
View File

@@ -0,0 +1,93 @@
// @ts-check
const path = require('path')
const execa = require('execa')
const resolveFrom = require('resolve-from')
const SEMVER_TYPES = ['patch', 'minor', 'major']
async function main() {
const args = process.argv
const releaseType = args[args.indexOf('--release-type') + 1]
const semverType = args[args.indexOf('--semver-type') + 1]
const isCanary = releaseType === 'canary'
const isReleaseCandidate = releaseType === 'release-candidate'
const isBeta = releaseType === 'beta'
if (
releaseType !== 'stable' &&
releaseType !== 'canary' &&
releaseType !== 'release-candidate' &&
releaseType !== 'beta'
) {
console.log(
`Invalid release type ${releaseType}, must be stable, canary, release-candidate, or beta`
)
return
}
if (!isCanary && !SEMVER_TYPES.includes(semverType)) {
console.log(
`Invalid semver type ${semverType}, must be one of ${SEMVER_TYPES.join(
', '
)}`
)
return
}
const githubToken = process.env.RELEASE_BOT_GITHUB_TOKEN
if (!githubToken) {
console.log(`Missing RELEASE_BOT_GITHUB_TOKEN`)
return
}
const configStorePath = resolveFrom(
path.join(process.cwd(), 'node_modules/release'),
'configstore'
)
const ConfigStore = require(configStorePath)
const config = new ConfigStore('release')
config.set('token', githubToken)
await execa(
`git remote set-url origin https://nextjs-bot:${githubToken}@github.com/vercel/next.js.git`,
{ stdio: 'inherit', shell: true }
)
await execa(`git config user.name "nextjs-bot"`, {
stdio: 'inherit',
shell: true,
})
await execa(`git config user.email "it+nextjs-bot@vercel.com"`, {
stdio: 'inherit',
shell: true,
})
console.log(`Running pnpm release-${isCanary ? 'canary' : 'stable'}...`)
const preleaseType =
semverType === 'major'
? 'premajor'
: semverType === 'minor'
? 'preminor'
: 'prerelease'
const child = execa(
isCanary
? `pnpm lerna version ${preleaseType} --preid canary --force-publish -y && pnpm release --pre --skip-questions --show-url`
: isReleaseCandidate
? `pnpm lerna version ${preleaseType} --preid rc --force-publish -y && pnpm release --pre --skip-questions --show-url`
: isBeta
? `pnpm lerna version ${preleaseType} --preid beta --force-publish -y && pnpm release --pre --skip-questions --show-url`
: `pnpm lerna version ${semverType} --force-publish -y`,
{
stdio: 'pipe',
shell: true,
}
)
child.stdout?.pipe(process.stdout)
child.stderr?.pipe(process.stderr)
await child
console.log('Release process is finished')
}
main()

86
scripts/sweep.cjs Normal file
View File

@@ -0,0 +1,86 @@
// This script must be run with tsx
const { existsSync, rmSync, readdirSync } = require('fs')
const { join } = require('path')
const { NEXT_DIR, exec, logCommand } = require('./pack-util')
const sweepInstalled = existsSync(`${process.env.CARGO_HOME}/bin/cargo-sweep`)
const cacheInstalled = existsSync(`${process.env.CARGO_HOME}/bin/cargo-cache`)
function removeNestedNext(directory) {
const items = readdirSync(directory, { withFileTypes: true })
for (const item of items) {
const fullPath = join(directory, item.name)
if (item.isDirectory()) {
if (item.name === 'node_modules' || item.name === '.git') {
// skip
} else if (item.name === '.next') {
console.log(`removing ${fullPath}`)
rmSync(fullPath, { recursive: true, force: true })
} else {
removeNestedNext(fullPath)
}
}
}
}
logCommand(`Remove .next directories`)
removeNestedNext(NEXT_DIR)
logCommand(`Remove .cache directories`)
rmSync(join(NEXT_DIR, 'node_modules/.cache'), { recursive: true, force: true })
rmSync('target/rust-analyzer/debug/incremental', {
recursive: true,
force: true,
})
function removeDirs(title, prefix) {
logCommand(title)
rmSync(`${prefix}target/tmp`, { recursive: true, force: true })
rmSync(`${prefix}target/release/incremental`, {
recursive: true,
force: true,
})
rmSync(`${prefix}target/debug/incremental`, { recursive: true, force: true })
}
removeDirs('Remove incremental dirs', '')
exec('Prune pnpm', 'pnpm prune', {
env: {
...process.env,
// We don't need to download the native build as we are not going to use it
NEXT_SKIP_NATIVE_POSTINSTALL: '1',
},
})
exec('Prune pnpm store', 'pnpm store prune')
if (!sweepInstalled) exec('Install cargo-sweep', 'cargo install cargo-sweep')
if (existsSync('target')) {
exec('Sweep', 'cargo sweep --maxsize 20000')
}
function chunkArray(array, chunkSize) {
const chunks = []
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize))
}
return chunks
}
// git tag -d $(git tag -l)
const tags = exec('Get tags', `git tag -l`, {
stdio: ['inherit', 'pipe', 'inherit'],
})
.toString()
.trim()
.split('\n')
for (const someTags of chunkArray(tags, 100)) {
exec('Delete local tags', `git tag -d ${someTags.join(' ')}`)
}
exec('Fetch & prune', 'git fetch -p')
exec('Git GC', 'git gc --prune=1day')
if (!cacheInstalled) exec('Install cargo-cache', 'cargo install cargo-cache')
exec('Optimize cargo cache', 'cargo cache -e')

690
scripts/sync-react.js Normal file
View File

@@ -0,0 +1,690 @@
// @ts-check
const path = require('path')
const fsp = require('fs/promises')
const process = require('process')
const { pathToFileURL } = require('url')
const execa = require('execa')
const { Octokit } = require('octokit')
const SemVer = require('semver')
const yargs = require('yargs')
// Use this script to update Next's vendored copy of React and related packages:
//
// Basic usage (defaults to most recent React canary version):
// pnpm run sync-react
//
// Update package.json but skip installing the dependencies automatically:
// pnpm run sync-react --no-install
//
// Sync from a local checkout of React (requires having React built first):
// pnpm run sync-react --version /path/to/react/checkout/
// Sync from a React commit (can be a commit on a PR)
// pnpm run sync-react --version vp:///commit-sha
const repoOwner = 'vercel'
const repoName = 'next.js'
const pullRequestLabels = ['type: react-sync']
const pullRequestReviewers = ['eps1lon']
/**
* Set to `null` to automatically sync the React version of Pages Router with App Router React version.
* Set to a specific version to override the Pages Router React version e.g. `^19.0.0`.
*
* "Active" just refers to our current development practice. While we do support
* React 18 in pages router, we don't focus our development process on it considering
* it does not receive new features.
* @type {string | null}
*/
const activePagesRouterReact = '^19.0.0'
const defaultLatestChannel = 'canary'
const filesReferencingReactPeerDependencyVersion = [
'run-tests.js',
'packages/create-next-app/templates/index.ts',
'test/lib/next-modes/base.ts',
]
const libraryManifestsSupportingNextjsReact = [
'packages/third-parties/package.json',
'packages/next/package.json',
]
const appManifestsInstallingNextjsPeerDependencies = [
'examples/reproduction-template/package.json',
'test/.stats-app/package.json',
// TODO: These should use the usual test helpers that automatically install the right React version
'test/e2e/next-test/first-time-setup-js/package.json',
'test/e2e/next-test/first-time-setup-ts/package.json',
]
async function getSchedulerVersion(reactVersion) {
if (reactVersion.startsWith('file://')) {
return reactVersion
}
if (reactVersion.startsWith('vp:')) {
return reactVersion
}
const url = `https://registry.npmjs.org/react-dom/${reactVersion}`
const response = await fetch(url, {
headers: {
Accept: 'application/json',
},
})
if (!response.ok) {
throw new Error(
`${url}: ${response.status} ${response.statusText}\n${await response.text()}`
)
}
const manifest = await response.json()
return manifest.dependencies['scheduler']
}
/**
* @param {string} packageName
* @param {string} versionStr An NPM version or a file URL to a React checkout
* @returns {string}
*/
function getPackageVersion(packageName, versionStr) {
if (versionStr.startsWith('file://')) {
return new URL(packageName, versionStr).href
}
if (versionStr.startsWith('vp:')) {
const { pathname } = new URL(versionStr)
const [, commit, releaseChannel] = pathname.split('/')
return new URL(
`/react/commits/${commit}/${packageName}@${releaseChannel}`,
'https://vercel-packages.vercel.app'
).href
}
return `npm:${packageName}@${versionStr}`
}
async function sync({ channel, newVersionStr, noInstall }) {
const useExperimental = channel === 'experimental'
const cwd = process.cwd()
const pkgJson = JSON.parse(
await fsp.readFile(path.join(cwd, 'package.json'), 'utf-8')
)
const devDependencies = pkgJson.devDependencies
const pnpmOverrides = pkgJson.pnpm.overrides
const baseVersionStr = devDependencies[
useExperimental ? 'react-experimental-builtin' : 'react-builtin'
].replace(/^npm:react@/, '')
console.log(`Updating "react@${channel}" to ${newVersionStr}...`)
if (newVersionStr === baseVersionStr) {
console.log('Already up to date.')
return
}
const newSchedulerVersionStr = await getSchedulerVersion(newVersionStr)
console.log(`Updating "scheduler@${channel}" to ${newSchedulerVersionStr}...`)
for (const packageName of ['react', 'react-dom']) {
devDependencies[
`${packageName}${useExperimental ? '-experimental' : ''}-builtin`
] = getPackageVersion(packageName, newVersionStr)
if (!useExperimental) {
pnpmOverrides[packageName] = getPackageVersion(packageName, newVersionStr)
}
}
for (const packageName of [
'react-server-dom-turbopack',
'react-server-dom-webpack',
]) {
devDependencies[`${packageName}${useExperimental ? '-experimental' : ''}`] =
getPackageVersion(packageName, newVersionStr)
}
devDependencies[
`scheduler-${useExperimental ? 'experimental-' : ''}builtin`
] = getPackageVersion('scheduler', newSchedulerVersionStr)
if (!useExperimental) {
pnpmOverrides.scheduler = getPackageVersion(
'scheduler',
newSchedulerVersionStr
)
// TODO: Should be handled like the other React packages
devDependencies['react-is-builtin'] = newVersionStr.startsWith('file://')
? new URL('react-is', newVersionStr).href
: newVersionStr.startsWith('vp:')
? getPackageVersion('react-is', newVersionStr)
: `npm:react-is@${newVersionStr}`
pnpmOverrides['react-is'] = newVersionStr.startsWith('file://')
? new URL('react-is', newVersionStr).href
: newVersionStr.startsWith('vp:')
? getPackageVersion('react-is', newVersionStr)
: `npm:react-is@${newVersionStr}`
}
await fsp.writeFile(
path.join(cwd, 'package.json'),
JSON.stringify(pkgJson, null, 2) +
// Prettier would add a newline anyway so do it manually to skip the additional `pnpm prettier-write`
'\n'
)
}
/**
* @typedef {object} ReactVersionInfo
* @property {string} semverVersion - The semver version of React.
* @property {string} releaseLabel - The release label of React (e.g. "canary", "rc").
* @property {string} sha - The commit SHA of the React version.
* @property {string} dateString - The date string of the React version.
* @returns {ReactVersionInfo}
*/
function extractInfoFromReactVersion(versionStr) {
if (versionStr.startsWith('file://')) {
return {
dateString: new Date().toISOString().split('T')[0],
releaseLabel: 'local',
semverVersion: '0.0.0',
sha: 'local',
}
}
if (versionStr.startsWith('vp:')) {
const { pathname } = new URL(versionStr)
const [, commit] = pathname.split('/')
return {
dateString: new Date().toISOString().split('T')[0],
releaseLabel: 'vercel-packages',
semverVersion: '0.0.0',
sha: commit,
}
}
if (versionStr.startsWith('https:')) {
const url = new URL(versionStr)
if (url.hostname === 'vercel-packages.vercel.app') {
// e.g https://vercel-packages.vercel.app/react/commits/bc50ab4bffa17f507386554a8ef3c3ed4f37fe1b/react@canary
const [, , , commit] = url.pathname.split('/')
return {
dateString: new Date().toISOString().split('T')[0],
releaseLabel: `vercel-packages`,
semverVersion: '0.0.0',
sha: commit,
}
}
throw new Error(
`Unsupported URL '${versionStr}'. Only vercel-packages.vercel.app URLs are supported.`
)
}
const match = versionStr.match(
/(?<semverVersion>.*)-(?<releaseLabel>.*)-(?<sha>.*)-(?<dateString>.*)$/
)
return match ? match.groups : null
}
async function getChangelogFromGitHub(baseSha, newSha) {
const pageSize = 50
let changelog = []
for (let currentPage = 1; ; currentPage++) {
const url = `https://api.github.com/repos/facebook/react/compare/${baseSha}...${newSha}?per_page=${pageSize}&page=${currentPage}`
const headers = new Headers()
// GITHUB_TOKEN is optional but helps in case of rate limiting during development.
if (process.env.GITHUB_TOKEN) {
headers.set('Authorization', `token ${process.env.GITHUB_TOKEN}`)
}
const response = await fetch(url, {
headers,
})
if (!response.ok) {
throw new Error(
`${response.url}: Failed to fetch commit log from GitHub:\n${response.statusText}\n${await response.text()}`
)
}
const data = await response.json()
const { commits } = data
for (const { commit, sha } of commits) {
const title = commit.message.split('\n')[0] || ''
const match =
// The "title" looks like "[Fiber][Float] preinitialized stylesheets should support integrity option (#26881)"
/\(#([0-9]+)\)$/.exec(title) ??
// or contains "Pull Request resolved: https://github.com/facebook/react/pull/12345" in the body if merged via ghstack (e.g. https://github.com/facebook/react/commit/0a0a5c02f138b37e93d5d93341b494d0f5d52373)
/^Pull Request resolved: https:\/\/github.com\/facebook\/react\/pull\/([0-9]+)$/m.exec(
commit.message
)
const prNum = match ? match[1] : ''
if (prNum) {
changelog.push(`- https://github.com/facebook/react/pull/${prNum}`)
} else {
changelog.push(
`- [${commit.message.split('\n')[0]} facebook/react@${sha.slice(0, 9)}](https://github.com/facebook/react/commit/${sha}) (${commit.author.name})`
)
}
}
if (commits.length < pageSize) {
// If the number of commits is less than the page size, we've reached
// the end. Otherwise we'll keep fetching until we run out.
break
}
}
changelog.reverse()
return changelog.length > 0 ? changelog.join('\n') : null
}
async function findHighestNPMReactVersion(versionLike) {
const { stdout, stderr } = await execa(
'npm',
['--silent', 'view', '--json', `react@${versionLike}`, 'version'],
{
// Avoid "Usage Error: This project is configured to use pnpm".
cwd: '/tmp',
}
)
if (stderr) {
console.error(stderr)
throw new Error(
`Failed to read highest react@${versionLike} version from npm.`
)
}
const result = JSON.parse(stdout)
return typeof result === 'string'
? result
: result.sort((a, b) => {
return SemVer.compare(b, a)
})[0]
}
async function main() {
const cwd = process.cwd()
const errors = []
const argv = await yargs(process.argv.slice(2))
.version(false)
.options('actor', {
type: 'string',
description:
'Required with `--create-pull`. The actor (GitHub username) that runs this script. Will be used for notifications but not commit attribution.',
})
.options('create-pull', {
default: false,
type: 'boolean',
description: 'Create a Pull Request in vercel/next.js',
})
.options('commit', {
default: false,
type: 'boolean',
description:
'Creates commits for each intermediate step. Useful to create better diffs for GitHub.',
})
.options('install', { default: true, type: 'boolean' })
.options('version', {
default: null,
type: 'string',
description:
'e.g. 19.3.0-canary-?-? or vp:///commit-sha for a build from a specific React commit (can be a commit on a PR)',
}).argv
let { actor, createPull, commit, install, version } = argv
if (version !== null && version.startsWith('/')) {
version = pathToFileURL(version).href
// Ensure trailing slash so that the URL is treated as a directory.
if (!version.endsWith('/')) {
version += '/'
}
}
async function commitEverything(message) {
await execa('git', ['add', '-A'])
await execa('git', [
'commit',
'--message',
message,
'--no-verify',
// Some steps can be empty, e.g. when we don't sync Pages router
'--allow-empty',
])
}
if (createPull && !actor) {
throw new Error(
`Pull Request cannot be created without a GitHub actor (received '${String(actor)}'). ` +
'Pass an actor via `--actor "some-actor"`.'
)
}
const githubToken = process.env.GITHUB_TOKEN
if (createPull && !githubToken) {
throw new Error(
`Environment variable 'GITHUB_TOKEN' not specified but required when --create-pull is specified.`
)
}
let newVersionStr = version
if (
newVersionStr === null ||
// TODO: Fork arguments in GitHub workflow to ensure `--version ""` is considered a mistake
newVersionStr === ''
) {
newVersionStr = await findHighestNPMReactVersion(defaultLatestChannel)
console.log(
`--version was not provided. Using react@${defaultLatestChannel}: ${newVersionStr}`
)
}
const newVersionInfo = extractInfoFromReactVersion(newVersionStr)
if (!newVersionInfo) {
throw new Error(
`New react version does not match expected format: ${newVersionStr}
Choose a React canary version from npm: https://www.npmjs.com/package/react?activeTab=versions
Or, run this command with no arguments to use the most recently published version.
`
)
}
const {
sha: newSha,
dateString: newDateString,
releaseLabel,
} = newVersionInfo
const branchName =
releaseLabel === 'local'
? // left to user to name their local sync branch
`update/react/local`
: releaseLabel === 'vercel-packages'
? `update/react/remote/vercel-packages/${newSha}`
: `update/react/${newVersionStr}`
if (createPull) {
const { exitCode, all, command } = await execa(
'git',
[
'ls-remote',
'--exit-code',
'--heads',
'origin',
`refs/heads/${branchName}`,
],
{ reject: false }
)
if (exitCode === 2) {
console.log(
`No sync in progress in branch '${branchName}' according to '${command}'. Starting a new one.`
)
} else if (exitCode === 0) {
console.log(
`An existing sync already exists in branch '${branchName}'. Delete the branch to start a new sync.`
)
return
} else {
throw new Error(
`Failed to check if the branch already existed:\n${command}: ${all}`
)
}
}
const rootManifest = JSON.parse(
await fsp.readFile(path.join(cwd, 'package.json'), 'utf-8')
)
const baseVersionStr = rootManifest.devDependencies['react-builtin'].replace(
/^npm:react@/,
''
)
let experimentalNewVersionStr = `0.0.0-experimental-${newSha}-${newDateString}`
if (version !== null && version.startsWith('file://')) {
experimentalNewVersionStr = new URL('build/oss-experimental/', version).href
newVersionStr = new URL('build/oss-stable/', version).href
} else if (releaseLabel === 'vercel-packages') {
experimentalNewVersionStr = `vp:///${newSha}/experimental`
newVersionStr = `vp:///${newSha}/canary`
}
await sync({
newVersionStr: experimentalNewVersionStr,
noInstall: !install,
channel: 'experimental',
})
if (commit) {
await commitEverything('Update `react@experimental`')
}
await sync({
newVersionStr,
noInstall: !install,
channel: '<framework-stable>',
})
if (commit) {
await commitEverything('Update `react`')
}
const baseVersionInfo = extractInfoFromReactVersion(baseVersionStr)
if (!baseVersionInfo) {
throw new Error(
'Base react version does not match expected format: ' + baseVersionStr
)
}
const syncPagesRouterReact = activePagesRouterReact === null
const newActivePagesRouterReactVersion = syncPagesRouterReact
? newVersionStr
: activePagesRouterReact
const pagesRouterReactVersion = `^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ${newActivePagesRouterReactVersion}`
const highestPagesRouterReactVersion = await findHighestNPMReactVersion(
pagesRouterReactVersion
)
const { sha: baseSha, dateString: baseDateString } = baseVersionInfo
for (const fileName of filesReferencingReactPeerDependencyVersion) {
const filePath = path.join(cwd, fileName)
const previousSource = await fsp.readFile(filePath, 'utf-8')
const previousHighestVersionMatch = previousSource.match(
/const nextjsReactPeerVersion = "([^"]+)";/
)
if (previousHighestVersionMatch === null) {
errors.push(
new Error(
`${fileName}: Is this file still referencing the React peer dependency version?`
)
)
} else {
const updatedSource = previousSource.replace(
previousHighestVersionMatch[0],
`const nextjsReactPeerVersion = "${highestPagesRouterReactVersion}";`
)
if (updatedSource !== previousSource) {
await fsp.writeFile(filePath, updatedSource)
}
}
}
for (const fileName of appManifestsInstallingNextjsPeerDependencies) {
const packageJsonPath = path.join(cwd, fileName)
const packageJson = await fsp.readFile(packageJsonPath, 'utf-8')
const manifest = JSON.parse(packageJson)
if (manifest.dependencies['react']) {
manifest.dependencies['react'] = highestPagesRouterReactVersion
}
if (manifest.dependencies['react-dom']) {
manifest.dependencies['react-dom'] = highestPagesRouterReactVersion
}
await fsp.writeFile(
packageJsonPath,
JSON.stringify(manifest, null, 2) +
// Prettier would add a newline anyway so do it manually to skip the additional `pnpm prettier-write`
'\n'
)
}
if (commit) {
await commitEverything('Updated peer dependency references in apps')
}
for (const fileName of libraryManifestsSupportingNextjsReact) {
const packageJsonPath = path.join(cwd, fileName)
const packageJson = await fsp.readFile(packageJsonPath, 'utf-8')
const manifest = JSON.parse(packageJson)
// Need to specify last supported RC version to avoid breaking changes.
if (manifest.peerDependencies['react']) {
manifest.peerDependencies['react'] = pagesRouterReactVersion
}
if (manifest.peerDependencies['react-dom']) {
manifest.peerDependencies['react-dom'] = pagesRouterReactVersion
}
await fsp.writeFile(
packageJsonPath,
JSON.stringify(manifest, null, 2) +
// Prettier would add a newline anyway so do it manually to skip the additional `pnpm prettier-write`
'\n'
)
}
if (commit) {
await commitEverything('Updated peer dependency references in libraries')
}
// Install the updated dependencies and build the vendored React files.
if (!install) {
console.log('Skipping install step because --no-install flag was passed.')
} else {
console.log('Installing dependencies...')
const installSubprocess = execa('pnpm', [
'install',
// Pnpm freezes the lockfile by default in CI.
// However, we just changed versions so the lockfile is expected to be changed.
'--no-frozen-lockfile',
])
if (installSubprocess.stdout) {
installSubprocess.stdout.pipe(process.stdout)
}
try {
await installSubprocess
} catch (error) {
console.error(error)
throw new Error('Failed to install updated dependencies.')
}
if (commit) {
await commitEverything('Update lockfile')
}
console.log('Building vendored React files...\n')
const nccSubprocess = execa('pnpm', ['ncc-compiled'], {
cwd: path.join(cwd, 'packages', 'next'),
})
if (nccSubprocess.stdout) {
nccSubprocess.stdout.pipe(process.stdout)
}
try {
await nccSubprocess
} catch (error) {
console.error(error)
throw new Error('Failed to run ncc.')
}
if (commit) {
await commitEverything('ncc-compiled')
}
// Print extra newline after ncc output
console.log()
}
let prDescription = ''
if (newVersionInfo.releaseLabel === 'local') {
prDescription = "Can't generate a changelog for local builds"
} else {
if (syncPagesRouterReact) {
prDescription += `**breaking change for canary users: Bumps peer dependency of React from \`${baseVersionStr}\` to \`${pagesRouterReactVersion}\`**\n\n`
}
// Fetch the changelog from GitHub and print it to the console.
prDescription += `[diff facebook/react@${baseSha}...${newSha}](https://github.com/facebook/react/compare/${baseSha}...${newSha})\n\n`
try {
const changelog = await getChangelogFromGitHub(baseSha, newSha)
if (changelog === null) {
prDescription += `GitHub reported no changes between ${baseSha} and ${newSha}.`
} else {
prDescription += `<details>\n<summary>React upstream changes</summary>\n\n${changelog}\n\n</details>`
}
} catch (error) {
console.error(error)
prDescription +=
'\nFailed to fetch changelog from GitHub. Changes were applied, anyway.\n'
}
}
if (!install) {
console.log(
`
To finish upgrading, complete the following steps:
- Install the updated dependencies: pnpm install
- Build the vendored React files: (inside packages/next dir) pnpm ncc-compiled
Or run this command again without the --no-install flag to do both automatically.
`
)
}
if (errors.length) {
// eslint-disable-next-line no-undef -- Defined in Node.js
throw new AggregateError(errors)
}
if (createPull) {
const octokit = new Octokit({ auth: githubToken })
const prTitle = `Upgrade React from \`${baseSha}-${baseDateString}\` to \`${newSha}-${newDateString}\``
await execa('git', ['checkout', '-b', branchName])
// We didn't commit intermediate steps yet so now we need to commit to create a PR.
if (!commit) {
commitEverything(prTitle)
}
await execa('git', ['push', 'origin', branchName])
const pullRequest = await octokit.rest.pulls.create({
owner: repoOwner,
repo: repoName,
head: branchName,
base: process.env.GITHUB_REF || 'canary',
draft: false,
title: prTitle,
body: prDescription,
})
console.log('Created pull request %s', pullRequest.data.html_url)
await Promise.all([
actor
? octokit.rest.issues.addAssignees({
owner: repoOwner,
repo: repoName,
issue_number: pullRequest.data.number,
assignees: [actor],
})
: Promise.resolve(),
octokit.rest.pulls.requestReviewers({
owner: repoOwner,
repo: repoName,
pull_number: pullRequest.data.number,
reviewers: pullRequestReviewers,
}),
octokit.rest.issues.addLabels({
owner: repoOwner,
repo: repoName,
issue_number: pullRequest.data.number,
labels: pullRequestLabels,
}),
])
}
console.log(prDescription)
console.log(
`Successfully updated React from \`${baseSha}-${baseDateString}\` to \`${newSha}-${newDateString}\``
)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})

191
scripts/test-new-tests.mjs Normal file
View File

@@ -0,0 +1,191 @@
// @ts-check
import execa from 'execa'
import yargs from 'yargs'
import getChangedTests from './get-changed-tests.mjs'
/**
* Run tests for added/changed tests in the current branch
* CLI Options:
* --mode: test mode (dev, deploy, start)
* --group: current group number / total groups
* --flake-detection: run tests multiple times to detect flaky
*/
async function main() {
const argv = await yargs(process.argv.slice(2))
.string('mode')
.string('group')
.boolean('flake-detection').argv
const testMode = argv.mode
const isFlakeDetectionMode = argv['flake-detection']
const attempts = isFlakeDetectionMode ? 3 : 1
if (testMode && !['dev', 'deploy', 'start'].includes(testMode)) {
throw new Error(
`Invalid test mode: ${testMode}. Must be one of: dev, deploy, start`
)
}
const rawGroup = argv['group']
let currentGroup = 1
let groupTotal = 1
if (rawGroup) {
;[currentGroup, groupTotal] = rawGroup
.split('/')
.map((item) => Number(item))
}
/** @type import('execa').Options */
const EXECA_OPTS = { shell: true }
/** @type import('execa').Options */
const EXECA_OPTS_STDIO = { ...EXECA_OPTS, stdio: 'inherit' }
const { devTests, prodTests, deployTests, commitSha } =
await getChangedTests()
let currentTests =
testMode === 'dev'
? devTests
: testMode === 'deploy'
? deployTests
: prodTests
/**
@type {Array<string[]>}
*/
const fileGroups = []
for (const test of currentTests) {
let smallestGroup = fileGroups[0]
let smallestGroupIdx = 0
// get the smallest group time to add current one to
for (let i = 0; i < groupTotal; i++) {
if (!fileGroups[i]) {
fileGroups[i] = []
}
if (
smallestGroup &&
fileGroups[i] &&
fileGroups[i].length < smallestGroup.length
) {
smallestGroup = fileGroups[i]
smallestGroupIdx = i
}
}
fileGroups[smallestGroupIdx].push(test)
}
currentTests = fileGroups[currentGroup - 1] || []
if (currentTests.length === 0) {
console.log(`No added/changed tests detected`)
return
}
const RUN_TESTS_ARGS = ['run-tests.js', '-c', '1', '--retries', '0']
// Only override the test version for deploy tests, as they need to run against
// the artifacts for the pull request. Otherwise, we don't need to specify this property,
// as tests will run against the local version of Next.js.
// Always use the commit SHA endpoint to avoid GitHub API rate limits on the
// PR number endpoint (which resolves the PR to a SHA on every request).
const nextTestVersion =
testMode === 'deploy'
? `https://vercel-packages.vercel.app/next/commits/${commitSha}/next`
: undefined
if (nextTestVersion) {
console.log(`Verifying artifacts for commit ${commitSha}`)
// Attempt to fetch the deploy artifacts for the commit
// These might take a moment to become available, so we'll retry a few times
const fetchWithRetry = async (url, retries = 5, timeout = 5000) => {
for (let i = 0; i < retries; i++) {
const res = await fetch(url)
if (res.ok) {
return res
} else if (i < retries - 1) {
console.log(
`Attempt ${i + 1} failed. Retrying in ${timeout / 1000} seconds...`
)
await new Promise((resolve) => setTimeout(resolve, timeout))
} else {
if (res.status === 404) {
throw new Error(
`Artifacts not found for commit ${commitSha}. ` +
`This can happen if the preview builds either failed or didn't succeed yet. ` +
`Once the "Deploy Preview tarball" job has finished, a retry should fix this error.`
)
}
throw new Error(
`Failed to verify artifacts for commit ${commitSha}: ${res.status}`
)
}
}
}
try {
await fetchWithRetry(nextTestVersion)
console.log(`Artifacts verified for commit ${commitSha}`)
} catch (error) {
console.error(error.message)
throw error
}
}
// We apply the external tests filter before the process.env so that if
// it's defined in the environment, it overrides the default filter.
// This is required for supporting the experimental tests setup.
const NEXT_EXTERNAL_TESTS_FILTERS = process.env.NEXT_EXTERNAL_TESTS_FILTERS
? process.env.NEXT_EXTERNAL_TESTS_FILTERS
: testMode === 'deploy'
? 'test/deploy-tests-manifest.json'
: undefined
if (NEXT_EXTERNAL_TESTS_FILTERS) {
console.log(
`Applying external tests filter: ${NEXT_EXTERNAL_TESTS_FILTERS}`
)
}
if (isFlakeDetectionMode) {
for (let i = 0; i < attempts; i++) {
console.log(
`\n\nRun ${i + 1}/${attempts} for ${testMode} tests (Turbopack)`
)
await execa('node', [...RUN_TESTS_ARGS, ...currentTests], {
...EXECA_OPTS_STDIO,
env: {
...process.env,
NEXT_TEST_MODE: testMode,
NEXT_TEST_VERSION: nextTestVersion,
NEXT_EXTERNAL_TESTS_FILTERS,
IS_TURBOPACK_TEST: '1',
TURBOPACK_BUILD:
testMode === 'start' || testMode === 'deploy' ? '1' : undefined,
TURBOPACK_DEV: testMode === 'dev' ? '1' : undefined,
},
})
}
} else {
for (let i = 0; i < attempts; i++) {
console.log(`\n\nRun ${i + 1}/${attempts} for ${testMode} tests`)
await execa('node', [...RUN_TESTS_ARGS, ...currentTests], {
...EXECA_OPTS_STDIO,
env: {
...process.env,
NEXT_EXTERNAL_TESTS_FILTERS,
NEXT_TEST_MODE: testMode,
NEXT_TEST_VERSION: nextTestVersion,
IS_WEBPACK_TEST: '1',
},
})
}
}
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

View File

@@ -0,0 +1,113 @@
#!/bin/bash
# Quick sanity check for stats benchmark config
# Tests that the dev server can start with the new config
set -e
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
echo "=== Stats Benchmark Config Test ==="
# Check Next.js is built
if [ ! -d "packages/next/dist" ]; then
echo "ERROR: Next.js not built. Run 'pnpm build' first."
exit 1
fi
# Create temp test app
WORK_DIR=$(mktemp -d)
trap "rm -rf $WORK_DIR" EXIT
echo "Setting up test app in $WORK_DIR..."
cp -r test/.stats-app/* "$WORK_DIR/"
cd "$WORK_DIR"
# Write the config that stats-config.js would write (with turbopack: {})
cat > next.config.js << 'EOF'
module.exports = {
generateBuildId: () => 'BUILD_ID',
turbopack: {},
}
EOF
echo "Config:"
cat next.config.js
echo ""
# Link local Next.js
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json'));
pkg.dependencies.next = 'file:$REPO_ROOT/packages/next';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
"
echo "Installing dependencies..."
pnpm install --ignore-scripts 2>/dev/null
# Test Turbopack dev (the failing scenario)
echo ""
echo "=== Test 1: Turbopack dev (default, no flag) ==="
PORT=$(python3 -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()")
rm -rf .next
NEXT_TELEMETRY_DISABLED=1 timeout 30 pnpm next dev --port $PORT > /tmp/turbo.log 2>&1 &
PID=$!
sleep 12
if kill -0 $PID 2>/dev/null; then
echo "OK: Turbopack dev server is running"
kill $PID 2>/dev/null || true
wait $PID 2>/dev/null || true
else
wait $PID 2>/dev/null || CODE=$?
if [ "$CODE" = "1" ]; then
echo "FAIL: Turbopack dev crashed (exit 1)"
echo "Output:"
cat /tmp/turbo.log
exit 1
fi
echo "OK: Process exited (timeout or normal)"
fi
# Test Webpack dev
echo ""
echo "=== Test 2: Webpack dev (--webpack flag) ==="
PORT=$(python3 -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()")
rm -rf .next
NEXT_TELEMETRY_DISABLED=1 timeout 30 pnpm next dev --webpack --port $PORT > /tmp/webpack.log 2>&1 &
PID=$!
sleep 12
if kill -0 $PID 2>/dev/null; then
echo "OK: Webpack dev server is running"
kill $PID 2>/dev/null || true
wait $PID 2>/dev/null || true
else
echo "OK: Process exited (timeout or normal)"
fi
# Test Turbopack build
echo ""
echo "=== Test 3: Turbopack build ==="
rm -rf .next
if NEXT_TELEMETRY_DISABLED=1 pnpm next build 2>&1 | head -20; then
echo "OK: Turbopack build completed"
else
echo "FAIL: Turbopack build failed"
exit 1
fi
# Test Webpack build
echo ""
echo "=== Test 4: Webpack build (--webpack flag) ==="
rm -rf .next
if NEXT_TELEMETRY_DISABLED=1 pnpm next build --webpack 2>&1 | head -20; then
echo "OK: Webpack build completed"
else
echo "FAIL: Webpack build failed"
exit 1
fi
echo ""
echo "=== All tests passed ==="

View File

@@ -0,0 +1,139 @@
#!/usr/bin/env node
/**
* CLI Startup Tracer
*
* Uses the V8 Inspector API to trace module loading at CLI startup.
* This helps identify which modules are being loaded eagerly.
*
* Usage:
* node scripts/trace-cli-startup.js [--command=dev|build|--help]
*/
const inspector = require('inspector')
const fs = require('fs')
const path = require('path')
const args = process.argv.slice(2)
const getArg = (name, defaultValue) => {
const arg = args.find((a) => a.startsWith(`--${name}=`))
return arg ? arg.split('=')[1] : defaultValue
}
const command = getArg('command', '--help')
const outputDir = path.join(process.cwd(), 'profiles')
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
console.log('\x1b[34m=== Next.js CLI Startup Trace ===\x1b[0m')
console.log(`Command: next ${command}`)
console.log(`Output directory: ${outputDir}`)
console.log('')
// Start CPU profiling
const session = new inspector.Session()
session.connect()
// Track module loading via require hook
const Module = require('module')
const originalRequire = Module.prototype.require
const loadedModules = []
const moduleLoadTimes = []
Module.prototype.require = function (id) {
const start = process.hrtime.bigint()
const result = originalRequire.apply(this, arguments)
const end = process.hrtime.bigint()
const durationMs = Number(end - start) / 1e6
// Get the resolved path
let resolvedPath = id
try {
resolvedPath = require.resolve(id, { paths: [this.path || process.cwd()] })
} catch {}
// Filter to show only Next.js-related modules
if (resolvedPath.includes('next/dist/') || resolvedPath.includes('@next/')) {
const shortPath = resolvedPath.includes('next/dist/')
? resolvedPath.split('next/dist/')[1]
: resolvedPath
if (!loadedModules.includes(shortPath)) {
loadedModules.push(shortPath)
moduleLoadTimes.push({ module: shortPath, time: durationMs })
}
}
return result
}
// Save original process.exit and intercept to prevent CLI from exiting mid-profile
const originalExit = process.exit
process.exit = () => {
// Don't actually exit during profiling - we want to capture the full profile
}
// Start profiling
session.post('Profiler.enable', () => {
session.post('Profiler.start', () => {
console.log('Starting CLI with profiling...')
console.log('')
const startTime = process.hrtime.bigint()
// Load the CLI
try {
process.argv = [process.argv[0], 'next', command]
require('../packages/next/dist/bin/next')
} catch (e) {
// Expected - CLI might throw
}
const endTime = process.hrtime.bigint()
const totalMs = Number(endTime - startTime) / 1e6
// Stop profiling and save
session.post('Profiler.stop', (err, { profile }) => {
if (err) {
console.error('Error stopping profiler:', err)
} else {
const profilePath = path.join(
outputDir,
`cli-startup-${Date.now()}.cpuprofile`
)
fs.writeFileSync(profilePath, JSON.stringify(profile))
console.log(`\x1b[32mProfile saved:\x1b[0m ${profilePath}`)
}
// Print results
console.log('')
console.log(`\x1b[32mTotal startup time:\x1b[0m ${totalMs.toFixed(2)}ms`)
console.log('')
console.log(`\x1b[33mModules loaded (${loadedModules.length}):\x1b[0m`)
console.log('='.repeat(70))
// Sort by load time
moduleLoadTimes.sort((a, b) => b.time - a.time)
moduleLoadTimes.slice(0, 30).forEach((m, i) => {
const timeStr =
m.time > 1
? `${m.time.toFixed(1)}ms`
: `${(m.time * 1000).toFixed(0)}μs`
console.log(`${String(i + 1).padStart(2)}. ${m.module} (${timeStr})`)
})
console.log('')
console.log(`\x1b[33mAll loaded modules:\x1b[0m`)
console.log(loadedModules.join('\n'))
// Restore original require
Module.prototype.require = originalRequire
session.disconnect()
// Exit cleanly now that profiling is complete
originalExit(0)
})
})
})

184
scripts/trace-dd.mjs Normal file
View File

@@ -0,0 +1,184 @@
import { createReadStream } from 'fs'
import { createInterface } from 'readline'
import path from 'path'
import Tracer from 'dd-trace'
import flat from 'flat'
const cleanFilename = (filename) => {
if (filename.includes('&absolutePagePath=')) {
filename =
'page ' +
decodeURIComponent(
filename.replace(/.+&absolutePagePath=/, '').slice(0, -1)
)
}
filename = filename.replace(/.+!(?!$)/, '')
return filename
}
const getPackageName = (filename) => {
const match = /.+[\\/]node_modules[\\/]((?:@[^\\/]+[\\/])?[^\\/]+)/.exec(
cleanFilename(filename)
)
return match && match[1]
}
/**
* Create, reports spans recursively with its inner child spans.
*/
const reportSpanRecursively = (tracer, trace, parentSpan) => {
// build-* span contains tags with path to the modules, trying to clean up if possible
const isBuildModule = trace.name.startsWith('build-module-')
if (isBuildModule) {
trace.packageName = getPackageName(trace.tags.name)
// replace name to cleaned up pkg name
trace.tags.name = trace.packageName
if (trace.children) {
const queue = [...trace.children]
trace.children = []
for (const e of queue) {
if (e.name.startsWith('build-module-')) {
const pkgName = getPackageName(e.tags.name)
if (!trace.packageName || pkgName !== trace.packageName) {
trace.children.push(e)
} else {
if (e.children) queue.push(...e.children)
}
}
}
}
}
/**
* interface TraceEvent {
* traceId: string;
* parentId: number;
* name: string;
* id: number;
* startTime: number;
* timestamp: number;
* duration: number;
* tags: Record<string, any>
* }
*/
let span = tracer.startSpan(trace.name, {
startTime: trace.startTime,
childOf: parentSpan,
tags: Object.keys(trace?.tags).length > 0 ? trace?.tags : undefined,
})
// Spans should be reported in chronological order
trace.children?.sort((a, b) => a.startTime - b.startTime)
trace.children?.forEach((childTrace) =>
reportSpanRecursively(tracer, childTrace, span)
)
span.finish(trace.startTime + trace.duration / 1000)
return span
}
/**
* Read generated trace from file system, augment & sent it to the remote tracer.
*/
const collectTraces = async (filePath, metadata) => {
const tracer = Tracer.init({
tags: metadata,
// Setting external env variable `DD_TRACE_DEBUG=true` will emit this log
logLevel: 'error',
// TODO: this is due to generated trace have excessive numbers of spans
// for build-module-*, using default flush causes overflow to the agent.
flushInterval: 20,
flushMinSpans: 10,
})
const readLineInterface = createInterface({
input: createReadStream(filePath),
crlfDelay: Infinity,
})
const traces = new Map()
const rootTraces = []
// Input trace file contains newline-separated sets of traces, where each line is valid JSON
// type of Array<TraceEvent>. Read it line-by-line to manually reconstruct trace trees.
//
// We have to read through end of the trace -
// Trace events in the input file can appear out of order, so we need to remodel the shape of the span tree before reporting
for await (const line of readLineInterface) {
JSON.parse(line).forEach((trace) => traces.set(trace.id, trace))
}
// Link inner, child spans to the parents to reconstruct span with correct relations
for (const event of traces.values()) {
if (event.parentId) {
event.parent = traces.get(event.parentId)
if (event.parent) {
if (!event.parent.children) event.parent.children = []
event.parent.children.push(event)
}
}
if (!event.parent) {
rootTraces.push(event)
}
}
for (const trace of rootTraces) {
reportSpanRecursively(tracer, trace)
}
}
/**
* Naively validate, collect necessary args.
*/
const validateArgs = async () => {
const { DD_ENV, DD_SERVICE, DATA_DOG_API_KEY } = process.env
if (!DATA_DOG_API_KEY) {
console.log(
"Skipping trace collection, api key is not available. Ensure 'DATA_DOG_API_KEY' env variable is set."
)
return
}
if (!DD_ENV || !DD_SERVICE) {
throw new Error(
`Could not find proper environment variables. Ensure to set DD_ENV / DD_SERVICE`
)
}
// Collect necessary default metadata. Script should pass cli args as in order of
// - trace file to read
// - which command ran to generated trace (`build`, `dev`, ...)
// - short sha for the commit
// - path to next.config.js (optional)
const [, , traceFilePath, command, commit, configFilePath] = process.argv
const config = configFilePath
? (await import(path.resolve(process.cwd(), configFilePath))).default
: {}
if (!traceFilePath || !command || !commit) {
throw new Error(
`Cannot collect traces without necessary metadata.
Try to run script with below args:
node trace-dd.mjs tracefilepath command commit [configfilepath]`
)
}
const metadata = {
command,
commit,
// TODO: it is unclear, but some of nested object seems not supported
nextjs_config: flat.flatten(config),
}
return [traceFilePath, metadata]
}
validateArgs()
.then(([traceFilePath, metadata]) => collectTraces(traceFilePath, metadata))
.catch((e) => {
console.error(`Failed to collect traces`)
console.error(e)
})

View File

@@ -0,0 +1,146 @@
const os = require('os')
const path = require('path')
const execa = require('execa')
const fsp = require('fs/promises')
const prettyBytes = require('pretty-bytes')
const gzipSize = require('next/dist/compiled/gzip-size')
const { nodeFileTrace } = require('next/dist/compiled/@vercel/nft')
const { linkPackages } =
require('../.github/actions/next-stats-action/src/prepare/repo-setup')()
const MAX_COMPRESSED_SIZE = 250 * 1000
const MAX_UNCOMPRESSED_SIZE = 2.5 * 1000 * 1000
// install next outside the monorepo for clean `node_modules`
// to trace against which helps ensure minimal trace is
// produced.
// react and react-dom need to be traced specific to installed
// version so isn't pre-traced
async function main() {
const tmpdir = os.tmpdir()
const origRepoDir = path.join(__dirname, '..')
const repoDir = path.join(tmpdir, `tmp-next-${Date.now()}`)
const workDir = path.join(tmpdir, `trace-next-${Date.now()}`)
const origTestDir = path.join(origRepoDir, 'test')
const dotDir = path.join(origRepoDir, './') + '.'
await fsp.cp(origRepoDir, repoDir, {
filter: (item) => {
return (
!item.startsWith(origTestDir) &&
!item.startsWith(dotDir) &&
!item.includes('node_modules')
)
},
force: true,
recursive: true,
})
console.log('using workdir', workDir)
console.log('using repodir', repoDir)
await fsp.mkdir(workDir, { recursive: true })
const pkgPaths = await linkPackages({
repoDir: origRepoDir,
nextSwcVersion: null,
})
await fsp.writeFile(
path.join(workDir, 'package.json'),
JSON.stringify(
{
dependencies: {
next: pkgPaths.get('next'),
},
private: true,
},
null,
2
)
)
await execa('yarn', ['install'], {
cwd: workDir,
stdio: ['ignore', 'inherit', 'inherit'],
env: {
...process.env,
YARN_CACHE_FOLDER: path.join(workDir, '.yarn-cache'),
},
})
const nextServerPath = path.join(
workDir,
'node_modules/next/dist/server/next-server.js'
)
const traceLabel = `traced ${nextServerPath}`
console.time(traceLabel)
const result = await nodeFileTrace([nextServerPath], {
base: workDir,
processCwd: workDir,
ignore: [
'node_modules/next/dist/pages/**/*',
'node_modules/next/dist/server/image-optimizer.js',
'node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*',
'node_modules/next/dist/compiled/webpack/(bundle4|bundle5).js',
'node_modules/react/**/*.development.js',
'node_modules/react-dom/**/*.development.js',
'node_modules/use-subscription/**/*.development.js',
'node_modules/sharp/**/*',
],
})
const tracedDeps = new Set()
let totalCompressedSize = 0
let totalUncompressedSize = 0
for (const file of result.fileList) {
if (result.reasons.get(file).type === 'initial') {
continue
}
tracedDeps.add(file.replace(/\\/g, '/'))
const stat = await fsp.stat(path.join(workDir, file))
if (stat.isFile()) {
const compressedSize = await gzipSize(path.join(workDir, file))
totalUncompressedSize += stat.size || 0
totalCompressedSize += compressedSize
} else {
console.log('not a file', file, stat.isDirectory())
}
}
console.log({
numberFiles: tracedDeps.size,
totalGzipSize: prettyBytes(totalCompressedSize),
totalUncompressedSize: prettyBytes(totalUncompressedSize),
})
await fsp.writeFile(
path.join(
__dirname,
'../packages/next/dist/server/next-server.js.nft.json'
),
JSON.stringify({
files: Array.from(tracedDeps),
version: 1,
})
)
await fsp.rm(workDir, { recursive: true, force: true })
await fsp.rm(repoDir, { recursive: true, force: true })
console.timeEnd(traceLabel)
if (
totalCompressedSize > MAX_COMPRESSED_SIZE ||
totalUncompressedSize > MAX_UNCOMPRESSED_SIZE
) {
throw new Error(
`Max traced size of next-server exceeded limits of ${MAX_COMPRESSED_SIZE} compressed or ${MAX_UNCOMPRESSED_SIZE} uncompressed`
)
}
}
main()
.then(() => console.log('done'))
.catch(console.error)

View File

@@ -0,0 +1,190 @@
import { createReadStream, createWriteStream } from 'fs'
import { createInterface } from 'readline'
import path from 'path'
import { EOL } from 'os'
const createEvent = (trace, ph, cat) => ({
name: trace.name,
// Category. We don't collect this for now.
cat: cat ?? '-',
ts: trace.timestamp,
// event category. We only use duration events (B/E) for now.
ph,
// process id. We don't collect this for now, putting arbitrary numbers.
pid: 1,
// thread id. We don't collect this for now, putting arbitrary numbers.
tid: 10,
args: trace.tags,
})
const cleanFilename = (filename) => {
if (filename.includes('&absolutePagePath=')) {
filename =
'page ' +
decodeURIComponent(
filename.replace(/.+&absolutePagePath=/, '').slice(0, -1)
)
}
filename = filename.replace(/.+!(?!$)/, '')
return filename
}
const getPackageName = (filename) => {
const match = /.+[\\/]node_modules[\\/]((?:@[^\\/]+[\\/])?[^\\/]+)/.exec(
cleanFilename(filename)
)
return match && match[1]
}
/**
* Create, reports spans recursively with its inner child spans.
*/
const reportSpanRecursively = (stream, trace, parentSpan) => {
// build-* span contains tags with path to the modules, trying to clean up if possible
const isBuildModule = trace.name.startsWith('build-module-')
if (isBuildModule) {
trace.packageName = getPackageName(trace.tags.name)
// replace name to cleaned up pkg name
trace.tags.name = trace.packageName
if (trace.children) {
const queue = [...trace.children]
trace.children = []
for (const e of queue) {
if (e.name.startsWith('build-module-')) {
const pkgName = getPackageName(e.tags.name)
if (!trace.packageName || pkgName !== trace.packageName) {
trace.children.push(e)
} else {
if (e.children) queue.push(...e.children)
}
}
}
}
}
/**
* interface TraceEvent {
* traceId: string;
* parentId: number;
* name: string;
* id: number;
* startTime: number;
* timestamp: number;
* duration: number;
* tags: Record<string, any>
* }
*/
stream.write(JSON.stringify(createEvent(trace, 'B')))
stream.write(',')
// Spans should be reported in chronological order
trace.children?.sort((a, b) => a.startTime - b.startTime)
trace.children?.forEach((childTrace) =>
reportSpanRecursively(stream, childTrace)
)
stream.write(
JSON.stringify(
createEvent(
{
...trace,
timestamp: trace.timestamp + trace.duration,
},
'E'
)
)
)
stream.write(',')
}
/**
* Read generated trace from file system, augment & sent it to the remote tracer.
*/
const collectTraces = async (filePath, outFilePath, metadata) => {
const readLineInterface = createInterface({
input: createReadStream(filePath),
crlfDelay: Infinity,
})
const writeStream = createWriteStream(outFilePath)
writeStream.write(`[${EOL}`)
const traces = new Map()
const rootTraces = []
// Input trace file contains newline-separated sets of traces, where each line is valid JSON
// type of Array<TraceEvent>. Read it line-by-line to manually reconstruct trace trees.
//
// We have to read through end of the trace -
// Trace events in the input file can appear out of order, so we need to remodel the shape of the span tree before reporting
for await (const line of readLineInterface) {
JSON.parse(line).forEach((trace) => traces.set(trace.id, trace))
}
// Link inner, child spans to the parents to reconstruct span with correct relations
for (const event of traces.values()) {
if (event.parentId) {
event.parent = traces.get(event.parentId)
if (event.parent) {
if (!event.parent.children) event.parent.children = []
event.parent.children.push(event)
}
}
if (!event.parent) {
rootTraces.push(event)
}
}
for (const trace of rootTraces) {
reportSpanRecursively(writeStream, trace)
}
writeStream.write(
JSON.stringify({
name: 'trace',
ph: 'M',
args: metadata,
})
)
writeStream.write(`${EOL}]`)
}
/**
* Naively validate, collect necessary args.
*/
const validateArgs = async () => {
// Collect necessary default metadata. Script should pass cli args as in order of
// - trace file to read
// - output file path (optional)
// - path to next.config.js (optional)
const [, , traceFilePath, outFile, configFilePath] = process.argv
const outFilePath = outFile ?? `${traceFilePath}.event`
const config = configFilePath
? (await import(path.resolve(process.cwd(), configFilePath))).default
: {}
if (!traceFilePath) {
throw new Error(
`Cannot collect traces without necessary metadata.
Try to run script with below args:
node trace-to-event-format.mjs tracefilepath [outfilepath] [configfilepath]`
)
}
const metadata = {
config,
}
return [traceFilePath, outFilePath, metadata]
}
validateArgs()
.then(([traceFilePath, outFilePath, metadata]) =>
collectTraces(traceFilePath, outFilePath, metadata)
)
.catch((e) => {
console.error(`Failed to generate traces`)
console.error(e)
})

245
scripts/trace-to-tree.mjs Normal file
View File

@@ -0,0 +1,245 @@
import fs from 'fs'
import eventStream from 'event-stream'
import {
bold,
blue,
cyan,
green,
magenta,
red,
yellow,
} from '../packages/next/dist/lib/picocolors.js'
const file = fs.createReadStream(process.argv[2])
const sum = (...args) => args.reduce((a, b) => a + b, 0)
const aggregate = (event) => {
const isBuildModule = event.name.startsWith('build-module-')
event.range = event.timestamp + (event.duration || 0)
event.total = isBuildModule ? event.duration : 0
if (isBuildModule) {
event.packageName = getPackageName(event.tags.name)
if (event.children) {
const queue = [...event.children]
event.children = []
event.childrenTimings = {}
event.mergedChildren = 0
for (const e of queue) {
if (!e.name.startsWith('build-module-')) {
event.childrenTimings[e.name] =
(event.childrenTimings[e.name] || 0) + e.duration
continue
}
const pkgName = getPackageName(e.tags.name)
if (!event.packageName || pkgName !== event.packageName) {
event.children.push(e)
} else {
event.duration += e.duration
event.mergedChildren++
if (e.children) queue.push(...e.children)
}
}
}
}
if (event.children) {
event.children.forEach(aggregate)
event.children.sort((a, b) => a.timestamp - b.timestamp)
event.range = Math.max(
event.range,
...event.children.map((c) => c.range || event.timestamp)
)
event.total += isBuildModule
? sum(...event.children.map((c) => c.total || 0))
: 0
}
}
const formatDuration = (duration, isBold) => {
const color = isBold ? bold : (x) => x
if (duration < 1000) {
return color(`${duration} µs`)
} else if (duration < 10000) {
return color(`${Math.round(duration / 100) / 10} ms`)
} else if (duration < 100000) {
return color(`${Math.round(duration / 1000)} ms`)
} else if (duration < 1_000_000) {
return color(cyan(`${Math.round(duration / 1000)} ms`))
} else if (duration < 10_000_000) {
return color(green(`${Math.round(duration / 100000) / 10} s`))
} else if (duration < 20_000_000) {
return color(yellow(`${Math.round(duration / 1000000)} s`))
} else if (duration < 100_000_000) {
return color(red(`${Math.round(duration / 1000000)} s`))
} else {
return color('🔥' + red(`${Math.round(duration / 1000000)} s`))
}
}
const formatTimes = (event) => {
const range = event.range - event.timestamp
const additionalInfo = []
if (event.total && event.total !== range)
additionalInfo.push(`total ${formatDuration(event.total)}`)
if (event.duration !== range)
additionalInfo.push(`self ${formatDuration(event.duration, bold)}`)
return `${formatDuration(range, additionalInfo.length === 0)}${
additionalInfo.length ? ` (${additionalInfo.join(', ')})` : ''
}`
}
const formatFilename = (filename) => {
return cleanFilename(filename).replace(/.+[\\/]node_modules[\\/]/, '')
}
const cleanFilename = (filename) => {
if (filename.includes('&absolutePagePath=')) {
filename =
'page ' +
decodeURIComponent(
filename.replace(/.+&absolutePagePath=/, '').slice(0, -1)
)
}
filename = filename.replace(/.+!(?!$)/, '')
return filename
}
const getPackageName = (filename) => {
const match = /.+[\\/]node_modules[\\/]((?:@[^\\/]+[\\/])?[^\\/]+)/.exec(
cleanFilename(filename)
)
return match && match[1]
}
const formatEvent = (event) => {
let head
switch (event.name) {
case 'webpack-compilation':
head = `${bold(`${event.tags.name} compilation`)} ${formatTimes(event)}`
break
case 'webpack-invalidated-client':
case 'webpack-invalidated-server':
head = `${bold(`${event.name.slice(-6)} recompilation`)} ${
event.tags.trigger === 'manual'
? '(new page discovered)'
: `(${formatFilename(event.tags.trigger)})`
} ${formatTimes(event)}`
break
case 'add-entry':
head = `${blue('entry')} ${formatFilename(event.tags.request)}`
break
case 'hot-reloader':
head = `${bold(green(`hot reloader`))}`
break
case 'export-page':
head = `${event.name} ${event.tags.path} ${formatTimes(event)}`
break
default:
if (event.name.startsWith('build-module-')) {
const { mergedChildren, childrenTimings, packageName } = event
head = `${magenta('module')} ${
packageName
? `${bold(cyan(packageName))} (${formatFilename(event.tags.name)}${
mergedChildren ? ` + ${mergedChildren}` : ''
})`
: formatFilename(event.tags.name)
} ${formatTimes(event)}`
if (childrenTimings && Object.keys(childrenTimings).length) {
head += ` [${Object.keys(childrenTimings)
.map((key) => `${key} ${formatDuration(childrenTimings[key])}`)
.join(', ')}]`
}
} else {
head = `${event.name} ${formatTimes(event)}`
}
break
}
if (event.children && event.children.length) {
return head + '\n' + treeChildren(event.children.map(formatEvent))
} else {
return head
}
}
const indentWith = (str, firstLinePrefix, otherLinesPrefix) => {
return firstLinePrefix + str.replace(/\n/g, '\n' + otherLinesPrefix)
}
const treeChildren = (items) => {
let str = ''
for (let i = 0; i < items.length; i++) {
if (i !== items.length - 1) {
str += indentWith(items[i], '├─ ', '│ ') + '\n'
} else {
str += indentWith(items[i], '└─ ', ' ')
}
}
return str
}
const tracesById = new Map()
file
.pipe(eventStream.split())
.pipe(
eventStream.mapSync((data) => {
if (!data) return
const json = JSON.parse(data)
json.forEach((event) => {
tracesById.set(event.id, event)
})
})
)
.on('end', () => {
const rootEvents = []
for (const event of tracesById.values()) {
if (event.parentId) {
event.parent = tracesById.get(event.parentId)
if (event.parent) {
if (!event.parent.children) event.parent.children = []
event.parent.children.push(event)
}
}
if (!event.parent) rootEvents.push(event)
}
for (const event of rootEvents) {
aggregate(event)
}
console.log(`Explanation:
${formatEvent({
name: 'build-module-js',
tags: { name: '/Users/next-user/src/magic-ui/pages/index.js' },
duration: 163000,
timestamp: 0,
range: 24000000,
total: 33000000,
childrenTimings: { 'read-resource': 873, 'next-babel-turbo-loader': 135000 },
})}
════════╤═══════════════════════════════════ ═╤═ ═╤═ ═╤════ ═══════════╤════════════════════════════════════════
└─ name of the processed module │ │ │ └─ timings of nested steps
│ │ └─ building the module itself (including overlapping parallel actions)
│ └─ total build time of this modules and all nested ones (including overlapping parallel actions)
└─ how long until the module and all nested modules took compiling (wall time, without overlapping actions)
${formatEvent({
name: 'build-module-js',
tags: {
name: '/Users/next-user/src/magic-ui/node_modules/lodash/camelCase.js',
},
packageName: 'lodash',
duration: 958000,
timestamp: 0,
range: 295000,
childrenTimings: { 'read-resource': 936000 },
mergedChildren: 281,
})}
═╤════ ══════╤════════════ ═╤═
│ │ └─ number of modules that are merged into that line
│ └─ first module that is imported
└─ npm package name
`)
for (const event of rootEvents) {
console.log(formatEvent(event))
}
})

30
scripts/unpack-next.ts Normal file
View File

@@ -0,0 +1,30 @@
// This script must be run with tsx
import { NEXT_DIR, exec } from './pack-util'
import fs from 'fs'
import path from 'path'
const TARBALLS = `${NEXT_DIR}/tarballs`
const PROJECT_DIR = path.resolve(process.argv[2])
function realPathIfAny(path: fs.PathLike) {
try {
return fs.realpathSync(path)
} catch {
return null
}
}
const packages = {
next: realPathIfAny(`${PROJECT_DIR}/node_modules/next`),
'next-swc': realPathIfAny(`${PROJECT_DIR}/node_modules/@next/swc`),
'next-mdx': realPathIfAny(`${PROJECT_DIR}/node_modules/@next/mdx`),
'next-bundle-analyzer': realPathIfAny(
`${PROJECT_DIR}/node_modules/@next/bundle-anlyzer`
),
}
for (const [key, path] of Object.entries(packages)) {
if (!path) continue
exec(`Unpack ${key}`, `tar -xf '${TARBALLS}/${key}.tar' -C '${path}'`)
}

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env node
const fs = require('fs/promises')
const path = require('path')
const fetch = require('node-fetch')
;(async () => {
const { familyMetadataList } = await fetch(
'https://fonts.google.com/metadata/fonts'
).then((r) => r.json())
let fontFunctions = `/**
* This is an autogenerated file by scripts/update-google-fonts.js
*/
import type { CssVariable, NextFont, NextFontWithVariable, Display } from '../types'
`
const fontData = {}
const ignoredSubsets = [
'menu',
'japanese',
'korean',
'chinese-simplified',
'chinese-hongkong',
'chinese-traditional',
]
for (let { family, fonts, axes, subsets } of familyMetadataList) {
subsets = subsets.filter((subset) => !ignoredSubsets.includes(subset))
const hasPreloadableSubsets = subsets.length > 0
const weights = new Set()
const styles = new Set()
for (const variant of Object.keys(fonts)) {
if (variant.endsWith('i')) {
styles.add('italic')
weights.add(variant.slice(0, -1))
continue
} else {
styles.add('normal')
weights.add(variant)
}
}
const hasVariableFont = axes.length > 0
let optionalAxes
if (hasVariableFont) {
weights.add('variable')
const nonWeightAxes = axes.filter(({ tag }) => tag !== 'wght')
if (nonWeightAxes.length > 0) {
optionalAxes = nonWeightAxes
}
}
fontData[family] = {
weights: [...weights],
styles: [...styles],
axes: hasVariableFont ? axes : undefined,
subsets,
}
const optionalIfVariableFont = hasVariableFont ? '?' : ''
const formatUnion = (values) =>
values.map((value) => `"${value}"`).join('|')
const weightTypes = [...weights]
const styleTypes = [...styles]
fontFunctions += `export declare function ${(/\d/.test(family[0])
? '_' + family
: family
).replaceAll(' ', '_')}
<T extends CssVariable | undefined = undefined>(options${optionalIfVariableFont}: {
weight${optionalIfVariableFont}:${formatUnion(
weightTypes
)} | Array<${formatUnion(
weightTypes.filter((weight) => weight !== 'variable')
)}>
style?: ${formatUnion(styleTypes)} | Array<${formatUnion(styleTypes)}>
display?:Display
variable?: T
${hasPreloadableSubsets ? 'preload?:boolean' : ''}
fallback?: string[]
adjustFontFallback?: boolean
${hasPreloadableSubsets ? `subsets?: Array<${formatUnion(subsets)}>` : ''}
${
optionalAxes
? `axes?:(${formatUnion(optionalAxes.map(({ tag }) => tag))})[]`
: ''
}
}): T extends undefined ? NextFont : NextFontWithVariable
`
}
await Promise.all([
fs.writeFile(
path.join(__dirname, '../packages/font/src/google/index.ts'),
fontFunctions
),
fs.writeFile(
path.join(__dirname, '../packages/font/src/google/font-data.json'),
JSON.stringify(fontData, null, 2)
),
])
})()

View File

@@ -0,0 +1,78 @@
const fs = require('fs')
const path = require('path')
const JSON5 = require('next/dist/compiled/json5')
const serverExternals = JSON5.parse(
fs.readFileSync(
path.join(
__dirname,
'../packages/next/src/lib/server-external-packages.jsonc'
),
'utf8'
)
)
function validate(docPath) {
const docContent = fs.readFileSync(
path.join(__dirname, '..', docPath),
'utf8'
)
const docPkgs = []
const extraPkgs = []
const missingPkgs = []
for (let docPkg of docContent
.split('opt-ed out:')
.pop()
.split('| Version')
.shift()
.split('\n')) {
docPkg = docPkg.split('`')[1]
if (!docPkg) {
continue
}
docPkgs.push(docPkg)
if (!serverExternals.includes(docPkg)) {
extraPkgs.push(docPkg)
}
}
for (const pkg of serverExternals) {
if (!docPkgs.includes(pkg)) {
missingPkgs.push(pkg)
}
}
if (extraPkgs.length || missingPkgs.length) {
console.log(
'server externals doc out of sync!\n' +
`Extra packages included: ` +
JSON.stringify(extraPkgs, null, 2) +
'\n' +
`Missing packages: ` +
JSON.stringify(missingPkgs, null, 2) +
'\n' +
`doc path: ${docPath}`
)
return false
}
return true
}
const appRouterValid = validate(
`docs/01-app/03-api-reference/05-config/01-next-config-js/serverExternalPackages.mdx`
)
const pagesRouterValid = validate(
`docs/02-pages/04-api-reference/04-config/01-next-config-js/serverExternalPackages.mdx`
)
if (appRouterValid && pagesRouterValid) {
console.log('server externals doc is in sync')
process.exit(0)
} else {
process.exit(1)
}