diff --git a/bench/module-cost/.gitignore b/bench/module-cost/.gitignore index fd0a8a0a91b506..b1d3827455c448 100644 --- a/bench/module-cost/.gitignore +++ b/bench/module-cost/.gitignore @@ -1,3 +1,4 @@ commonjs/* esm/* -CPU* \ No newline at end of file +CPU* +benchmark-results-*.json \ No newline at end of file diff --git a/bench/module-cost/components/client.js b/bench/module-cost/components/client.js index 967e20a312fa9d..f8118beb353a28 100644 --- a/bench/module-cost/components/client.js +++ b/bench/module-cost/components/client.js @@ -4,6 +4,11 @@ import { useEffect, useRef, useState } from 'react' import { format, measure } from '../lib/measure' function report(result, element, textarea) { + if (!globalThis.BENCHMARK_RESULTS) { + globalThis.BENCHMARK_RESULTS = [] + } + globalThis.BENCHMARK_RESULTS.push(result) + const formattedResult = format(result) element.textContent += `: ${formattedResult}` textarea.current.value += `\n ${formattedResult}` diff --git a/bench/module-cost/package.json b/bench/module-cost/package.json index d50bd133aaf556..eb6e8a7478d92a 100644 --- a/bench/module-cost/package.json +++ b/bench/module-cost/package.json @@ -2,6 +2,7 @@ "name": "module-cost", "scripts": { "prepare-bench": "node scripts/prepare-bench.mjs", + "benchmark": "node scripts/benchmark-runner.mjs", "dev-webpack": "next dev", "dev-turbopack": "next dev --turbo", "build-webpack": "next build", @@ -10,6 +11,7 @@ }, "devDependencies": { "rimraf": "6.0.1", - "next": "workspace:*" + "next": "workspace:*", + "playwright": "^1.40.0" } } diff --git a/bench/module-cost/scripts/benchmark-runner.mjs b/bench/module-cost/scripts/benchmark-runner.mjs new file mode 100644 index 00000000000000..eb96b27ce35716 --- /dev/null +++ b/bench/module-cost/scripts/benchmark-runner.mjs @@ -0,0 +1,255 @@ +import { spawn } from 'node:child_process' +import { writeFileSync } from 'node:fs' +import { chromium } from 'playwright' + +/// To use: +/// - Install Playwright: `npx playwright install chromium` +/// - Install dependencies: `pnpm install` +/// - Build the application: `pnpm build-webpack` or pnpm build-turbopack` +/// - Run the benchmark: `pnpm benchmark` + +class BenchmarkRunner { + constructor(options) { + this.name = options.name + this.samples = options.samples ?? 50 + this.buttonClickDelay = options.buttonClickDelay ?? 500 + this.results = [] + } + + async runBenchmark() { + for (let i = 1; i <= this.samples; i++) { + console.log(`\n--- Running sample ${i}/${this.samples} ---`) + + const result = await this.runSingleSample() + this.results.push(...result) + } + + this.saveResults() + console.log('\nBenchmark completed!') + } + + async runSingleSample() { + let server + let browser + + try { + // 1. Launch the server + server = await this.startServer() + + // 2. Launch Chrome incognito + console.log('Launching browser...') + browser = await chromium.launch({ + headless: true, // Set to true if you don't want to see the browser + args: ['--incognito'], + }) + + const context = await browser.newContext() + const page = await context.newPage() + + // 3. Navigate to localhost:3000 + await page.goto('http://localhost:3000', { waitUntil: 'load' }) + + // 4. Find and click all buttons + const buttons = await page.locator('button').all() + + for (let j = 0; j < buttons.length; j++) { + await buttons[j].click() + await this.sleep(this.buttonClickDelay) + } + + // 5. Capture data from textbox + console.log('Capturing data from the page...') + const textboxData = await this.capturePageData(page) + console.log('Captured data from the page:', textboxData) + + // 6. Close browser + console.log('Closing browser...') + await browser.close() + browser = null + + // 7. Shut down server + console.log('Shutting down server...') + await this.stopServer(server) + server = null + + return textboxData + } catch (error) { + // Cleanup in case of error + if (browser) { + try { + await browser.close() + } catch (e) { + console.error('Error closing browser:', e.message) + } + } + if (server) { + try { + await this.stopServer(server) + } catch (e) { + console.error('Error stopping server:', e.message) + } + } + throw error + } + } + + async startServer() { + return new Promise((resolve, reject) => { + const server = spawn('pnpm', ['start'], { + stdio: ['pipe', 'pipe', 'pipe'], + shell: true, + }) + + let serverReady = false + + server.stdout.on('data', (data) => { + const output = data.toString() + console.log('Server:', output.trim()) + + // Look for common Next.js ready indicators + if ( + output.includes('Ready') || + output.includes('started server') || + output.includes('Local:') + ) { + if (!serverReady) { + serverReady = true + resolve(server) + } + } + }) + + server.stderr.on('data', (data) => { + console.error('Server Error:', data.toString().trim()) + }) + + server.on('error', (error) => { + reject(new Error(`Failed to start server: ${error.message}`)) + }) + + server.on('close', (code) => { + if (!serverReady) { + reject( + new Error(`Server exited with code ${code} before becoming ready`) + ) + } + }) + + // Timeout after 30 seconds + setTimeout(() => { + if (!serverReady) { + server.kill() + reject(new Error('Server startup timeout')) + } + }, 30000) + }) + } + + async stopServer(server) { + return new Promise((resolve) => { + if (!server || server.killed) { + resolve() + return + } + + server.on('close', () => { + resolve() + }) + + // Try graceful shutdown first + server.kill('SIGTERM') + + // Force kill after 5 seconds + setTimeout(() => { + if (!server.killed) { + server.kill('SIGKILL') + } + resolve() + }, 5000) + }) + } + + async capturePageData(page) { + return await page.evaluate(() => globalThis.BENCHMARK_RESULTS) + } + + async sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + + saveResults() { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-') + const filename = `benchmark-results-${this.name}-${timestamp}.json` + + writeFileSync( + filename, + JSON.stringify(summarizeDurations(this.results), null, 2) + ) + console.log(`Results saved to ${filename}`) + } +} + +const summarizeDurations = (data) => { + if (!Array.isArray(data) || data.length === 0) { + throw new Error('No data to summarize') + } + + const byName = new Map() + for (const item of data) { + const name = item.name + if (!byName.has(name)) { + byName.set(name, []) + } + byName.get(name).push(item) + } + const results = [] + for (const [name, data] of byName) { + const loadDurations = data + .map((item) => item.loadDuration) + .sort((a, b) => a - b) + const executeDurations = data + .map((item) => item.executeDuration) + .sort((a, b) => a - b) + + const getSummary = (durations) => { + const sum = durations.reduce((acc, val) => acc + val, 0) + const average = sum / durations.length + + const middle = Math.floor(durations.length / 2) + const median = + durations.length % 2 === 0 + ? (durations[middle - 1] + durations[middle]) / 2 + : durations[middle] + + const percentile75Index = Math.floor(durations.length * 0.75) + const percentile75 = durations[percentile75Index] + + return { + average, + median, + percentile75, + } + } + + results.push({ + name, + totalSamples: data.length, + loadDuration: getSummary(loadDurations), + executeDuration: getSummary(executeDurations), + }) + } + + return results +} + +// CLI usage +const args = process.argv.slice(2) +const samples = args.length > 0 ? Number.parseInt(args[0]) : undefined +const name = args.length > 1 ? args[1] : undefined + +const runner = new BenchmarkRunner({ + name, + samples, +}) + +runner.runBenchmark().catch(console.error) diff --git a/crates/next-core/src/next_client/transforms.rs b/crates/next-core/src/next_client/transforms.rs index 1fa152e99b98ba..a51d033d0900f4 100644 --- a/crates/next-core/src/next_client/transforms.rs +++ b/crates/next-core/src/next_client/transforms.rs @@ -45,7 +45,8 @@ pub async fn get_next_client_transforms_rules( rules.push(get_next_font_transform_rule(enable_mdx_rs)); - if mode.await?.is_development() { + let is_development = mode.await?.is_development(); + if is_development { rules.push(get_debug_fn_name_rule(enable_mdx_rs)); } @@ -68,7 +69,11 @@ pub async fn get_next_client_transforms_rules( enable_mdx_rs, pages_dir.clone(), )); - rules.push(get_next_page_config_rule(enable_mdx_rs, pages_dir.clone())); + rules.push(get_next_page_config_rule( + is_development, + enable_mdx_rs, + pages_dir.clone(), + )); } } ClientContextType::App { .. } => { diff --git a/crates/next-core/src/next_shared/transforms/next_page_config.rs b/crates/next-core/src/next_shared/transforms/next_page_config.rs index c95fe13a926854..9c91e382f07220 100644 --- a/crates/next-core/src/next_shared/transforms/next_page_config.rs +++ b/crates/next-core/src/next_shared/transforms/next_page_config.rs @@ -9,10 +9,13 @@ use turbopack_ecmascript::{CustomTransformer, EcmascriptInputTransform, Transfor use super::module_rule_match_pages_page_file; -pub fn get_next_page_config_rule(enable_mdx_rs: bool, pages_dir: FileSystemPath) -> ModuleRule { +pub fn get_next_page_config_rule( + is_development: bool, + enable_mdx_rs: bool, + pages_dir: FileSystemPath, +) -> ModuleRule { let transformer = EcmascriptInputTransform::Plugin(ResolvedVc::cell(Box::new(NextPageConfig { - // [TODO]: update once turbopack build works - is_development: true, + is_development, }) as _)); ModuleRule::new( module_rule_match_pages_page_file(enable_mdx_rs, pages_dir), diff --git a/docs/01-app/03-api-reference/08-turbopack.mdx b/docs/01-app/03-api-reference/08-turbopack.mdx index 5aafcd4cd3b940..06bb270786246f 100644 --- a/docs/01-app/03-api-reference/08-turbopack.mdx +++ b/docs/01-app/03-api-reference/08-turbopack.mdx @@ -132,8 +132,6 @@ Turbopack can be configured via `next.config.js` (or `next.config.ts`) under the Change or extend file extensions for module resolution. - **`moduleIds`** Set how module IDs are generated (`'named'` vs `'deterministic'`). -- **`treeShaking`** - Enable or disable tree shaking in dev and future production builds. - **`memoryLimit`** Set a memory limit (in bytes) for Turbopack. diff --git a/lerna.json b/lerna.json index be7dbf64cac682..08d35255a24d0c 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "15.4.2-canary.29" + "version": "15.4.2-canary.30" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index eeadcb988eaec7..76378709edf287 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 15b7409185168a..f20f03df276aa0 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/api-reference/config/eslint", "dependencies": { - "@next/eslint-plugin-next": "15.4.2-canary.29", + "@next/eslint-plugin-next": "15.4.2-canary.30", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index e4919e29f9beea..4e070d7e0461b2 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,7 +1,7 @@ { "name": "@next/eslint-plugin-internal", "private": true, - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "description": "ESLint plugin for working on Next.js.", "exports": { ".": "./src/eslint-plugin-internal.js" diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index f3cd18c2d59da5..ea002317f5fbe8 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/font/package.json b/packages/font/package.json index 352ff38105d16e..a5af286c373242 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 6c39d178fcf644..d82160a91eb3bc 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 85cabc01e3472d..aeac419254a82c 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 8457e5915df23c..724a3a50eb7104 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index dc78d9e59ceb56..37e025eefe2114 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 3a4b64d8fa63ad..846c0e99d6ae3d 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 90a1e49c271ecc..3e1e03e03aed23 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index e43df0a08f540b..d948a3e8cdd73a 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index 713715173f3376..b9cb7fc9da8ba7 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index ef5752f126e587..5753073e50f0f6 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "private": true, "files": [ "native/" diff --git a/packages/next/package.json b/packages/next/package.json index 08663a41449813..3cb1085bd31f0e 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -102,7 +102,7 @@ ] }, "dependencies": { - "@next/env": "15.4.2-canary.29", + "@next/env": "15.4.2-canary.30", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -166,11 +166,11 @@ "@jest/types": "29.5.0", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/font": "15.4.2-canary.29", - "@next/polyfill-module": "15.4.2-canary.29", - "@next/polyfill-nomodule": "15.4.2-canary.29", - "@next/react-refresh-utils": "15.4.2-canary.29", - "@next/swc": "15.4.2-canary.29", + "@next/font": "15.4.2-canary.30", + "@next/polyfill-module": "15.4.2-canary.30", + "@next/polyfill-nomodule": "15.4.2-canary.30", + "@next/react-refresh-utils": "15.4.2-canary.30", + "@next/swc": "15.4.2-canary.30", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.51.1", "@rspack/core": "1.4.5", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 35f6a0ca70b1c2..601a1f089f14c0 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index 5f317e7a870b9a..33aae304ec1f63 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "15.4.2-canary.29", + "version": "15.4.2-canary.30", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "15.4.2-canary.29", + "next": "15.4.2-canary.30", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "5.8.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ee92e26da9698..27d73760c6940f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -676,6 +676,9 @@ importers: next: specifier: workspace:* version: link:../../packages/next + playwright: + specifier: ^1.40.0 + version: 1.48.0 rimraf: specifier: 6.0.1 version: 6.0.1 @@ -851,7 +854,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 15.4.2-canary.29 + specifier: 15.4.2-canary.30 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.10.3 @@ -921,7 +924,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 15.4.2-canary.29 + specifier: 15.4.2-canary.30 version: link:../next-env '@swc/helpers': specifier: 0.5.15 @@ -1046,19 +1049,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 15.4.2-canary.29 + specifier: 15.4.2-canary.30 version: link:../font '@next/polyfill-module': - specifier: 15.4.2-canary.29 + specifier: 15.4.2-canary.30 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 15.4.2-canary.29 + specifier: 15.4.2-canary.30 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 15.4.2-canary.29 + specifier: 15.4.2-canary.30 version: link:../react-refresh-utils '@next/swc': - specifier: 15.4.2-canary.29 + specifier: 15.4.2-canary.30 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1761,7 +1764,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 15.4.2-canary.29 + specifier: 15.4.2-canary.30 version: link:../next outdent: specifier: 0.8.0