From 73731574bed6b09f33de7921d74d76c056263035 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 6 May 2026 08:54:04 -0600 Subject: [PATCH 1/2] feat: add turbopack support for Next.js v15 and refactor bundler option Replace the `webpack: boolean` plugin option with a `bundler` option that accepts "webpack" or "turbopack". The default is version-aware: - v16: turbopack (matches Next.js v16 default) - v15: webpack (matches Next.js v15 default) - v14: webpack (no turbopack support) Users can now explicitly set `bundler: turbopack` for v15 to opt in, or `bundler: webpack` for v16 to opt out. - Add `next-15-turbopack` fixture and integration test - Update `next-16-webpack` fixture to use `bundler: webpack` Co-Authored-By: Claude Opus 4.6 (1M context) --- fixtures/next-15-turbopack/.npmrc | 1 + fixtures/next-15-turbopack/app/layout.js | 11 +++++++++ fixtures/next-15-turbopack/app/page.js | 7 ++++++ fixtures/next-15-turbopack/config.yaml | 3 +++ fixtures/next-15-turbopack/next.config.mjs | 3 +++ fixtures/next-15-turbopack/package.json | 16 +++++++++++++ fixtures/next-16-webpack/config.yaml | 2 +- integrationTests/next-15-turbopack.pw.ts | 18 +++++++++++++++ src/plugin.ts | 27 ++++++++++++++++------ 9 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 fixtures/next-15-turbopack/.npmrc create mode 100644 fixtures/next-15-turbopack/app/layout.js create mode 100644 fixtures/next-15-turbopack/app/page.js create mode 100644 fixtures/next-15-turbopack/config.yaml create mode 100644 fixtures/next-15-turbopack/next.config.mjs create mode 100644 fixtures/next-15-turbopack/package.json create mode 100644 integrationTests/next-15-turbopack.pw.ts diff --git a/fixtures/next-15-turbopack/.npmrc b/fixtures/next-15-turbopack/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/fixtures/next-15-turbopack/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/fixtures/next-15-turbopack/app/layout.js b/fixtures/next-15-turbopack/app/layout.js new file mode 100644 index 0000000..de69185 --- /dev/null +++ b/fixtures/next-15-turbopack/app/layout.js @@ -0,0 +1,11 @@ +export const metadata = { + title: 'Harper - Next.js v15 App', +}; + +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} diff --git a/fixtures/next-15-turbopack/app/page.js b/fixtures/next-15-turbopack/app/page.js new file mode 100644 index 0000000..4a48cf9 --- /dev/null +++ b/fixtures/next-15-turbopack/app/page.js @@ -0,0 +1,7 @@ +export default async function Page() { + return ( +
+

Next.js v15

+
+ ); +} diff --git a/fixtures/next-15-turbopack/config.yaml b/fixtures/next-15-turbopack/config.yaml new file mode 100644 index 0000000..ddeb17c --- /dev/null +++ b/fixtures/next-15-turbopack/config.yaml @@ -0,0 +1,3 @@ +'@harperfast/nextjs': + package: '@harperfast/nextjs' + bundler: turbopack diff --git a/fixtures/next-15-turbopack/next.config.mjs b/fixtures/next-15-turbopack/next.config.mjs new file mode 100644 index 0000000..ada3f1d --- /dev/null +++ b/fixtures/next-15-turbopack/next.config.mjs @@ -0,0 +1,3 @@ +import { withHarper } from '@harperfast/nextjs'; + +export default withHarper({}); diff --git a/fixtures/next-15-turbopack/package.json b/fixtures/next-15-turbopack/package.json new file mode 100644 index 0000000..bf94d84 --- /dev/null +++ b/fixtures/next-15-turbopack/package.json @@ -0,0 +1,16 @@ +{ + "name": "next-15", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@harperfast/nextjs": "file:../../", + "react": "^19", + "react-dom": "^19", + "next": "^15" + } +} diff --git a/fixtures/next-16-webpack/config.yaml b/fixtures/next-16-webpack/config.yaml index 7707779..e7c727f 100644 --- a/fixtures/next-16-webpack/config.yaml +++ b/fixtures/next-16-webpack/config.yaml @@ -1,3 +1,3 @@ '@harperfast/nextjs': package: '@harperfast/nextjs' - webpack: true + bundler: webpack diff --git a/integrationTests/next-15-turbopack.pw.ts b/integrationTests/next-15-turbopack.pw.ts new file mode 100644 index 0000000..9590e3b --- /dev/null +++ b/integrationTests/next-15-turbopack.pw.ts @@ -0,0 +1,18 @@ +import { fixture } from './fixture.ts'; + +const { test, expect } = fixture('next-15-turbopack'); + +test('home page renders', async ({ page, harper }) => { + await page.goto(harper.httpURL); + await expect(page.locator('h1')).toHaveText('Next.js v15'); +}); + +test('page title is set', async ({ page, harper }) => { + await page.goto(harper.httpURL); + await expect(page).toHaveTitle('Harper - Next.js v15 App'); +}); + +test('status endpoint returns 200', async ({ request, harper }) => { + const response = await request.get(`${harper.operationsAPIURL}/health`); + expect(response.status()).toBe(200); +}); diff --git a/src/plugin.ts b/src/plugin.ts index 30e048e..283ab3a 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -14,8 +14,11 @@ import type NextBuildModule15 from 'next-15/dist/cli/next-build.d.ts'; import type NextModule16 from 'next-16'; import type NextBuildModule16 from 'next-16/dist/cli/next-build.d.ts'; +type Bundler = 'webpack' | 'turbopack'; + interface NextPluginConfig extends Config { buildOnly: boolean; + bundler: Bundler; dev: boolean; // @ts-expect-error files?: FilesOption; @@ -23,7 +26,6 @@ interface NextPluginConfig extends Config { prebuilt: boolean; runFirst: boolean; securePort?: number; - webpack: boolean; } // Bringing this forward from extension since some validation is better than none. @@ -64,16 +66,22 @@ function resolveConfig(scope: Scope): NextPluginConfig { } assertType('buildOnly', options.buildOnly, 'boolean'); + assertType('bundler', options.bundler, 'string'); assertType('dev', options.dev, 'boolean'); assertType('port', options.port, 'number'); assertType('prebuilt', options.prebuilt, 'boolean'); assertType('runFirst', options.runFirst, 'boolean'); assertType('securePort', options.securePort, 'number'); - assertType('webpack', options.webpack, 'boolean'); + + if (options.bundler && options.bundler !== 'webpack' && options.bundler !== 'turbopack') { + throw new Error(`bundler must be "webpack" or "turbopack". Received: "${options.bundler}"`); + } // TODO: Remove type casts when we have more proper plugin option validation from core return { buildOnly: (options.buildOnly as boolean) ?? false, + // bundler default is set later in handleApplication() based on the detected Next.js version + bundler: options.bundler as Bundler, dev: (options.dev as boolean) ?? false, // @ts-expect-error files: options.files, @@ -81,7 +89,6 @@ function resolveConfig(scope: Scope): NextPluginConfig { prebuilt: (options.prebuilt as boolean) ?? false, runFirst: (options.runFirst as boolean) ?? false, securePort: options.securePort as number, - webpack: (options.webpack as boolean) ?? false, } satisfies NextPluginConfig; } @@ -184,6 +191,12 @@ export async function handleApplication(scope: Scope) { const next = importNext(scope); + // Set the bundler default based on the detected Next.js version if not explicitly configured. + // Next.js v16 defaults to turbopack; v14 and v15 default to webpack. + if (!config.bundler) { + config.bundler = next.version >= 16 ? 'turbopack' : 'webpack'; + } + scope.logger.debug?.(`Detected Next.js version: ${next.version}`); if (config.prebuilt) { @@ -297,7 +310,7 @@ async function build(scope: Scope, config: NextPluginConfig, next: NextPackage) { lint: false, mangling: true, - turbopack: false, + ...(config.bundler === 'turbopack' && { turbopack: true }), experimentalDebugMemoryUsage: false, experimentalBuildMode: 'default', }, @@ -308,7 +321,7 @@ async function build(scope: Scope, config: NextPluginConfig, next: NextPackage) await next.build( { mangling: true, - webpack: config.webpack, + ...(config.bundler === 'webpack' && { webpack: true }), experimentalDebugMemoryUsage: false, experimentalBuildMode: 'default', }, @@ -341,10 +354,10 @@ async function serve(scope: Scope, config: NextPluginConfig, next: NextPackage) app = next.server({ dir: scope.directory, dev: config.dev }); break; case 15: - app = next.server({ dir: scope.directory, dev: config.dev, turbopack: false }); + app = next.server({ dir: scope.directory, dev: config.dev, ...(config.bundler === 'turbopack' && { turbopack: true }) }); break; case 16: - app = next.server({ dir: scope.directory, dev: config.dev, ...(config.webpack && { webpack: true }) }); + app = next.server({ dir: scope.directory, dev: config.dev, ...(config.bundler === 'webpack' && { webpack: true }) }); break; } From 32e4f5e5c33ebcf894933b1c892bb814d9b6b48c Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 6 May 2026 10:00:26 -0600 Subject: [PATCH 2/2] feat: add v14 turbopack guard and validate all versions (#45) Next.js v14 does not support turbopack via the createServer or nextBuild APIs. Log an error and fall back to webpack if a user sets `bundler: turbopack` with a v14 application. Co-authored-by: Claude Opus 4.6 (1M context) --- src/plugin.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugin.ts b/src/plugin.ts index 283ab3a..fad6802 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -197,6 +197,11 @@ export async function handleApplication(scope: Scope) { config.bundler = next.version >= 16 ? 'turbopack' : 'webpack'; } + if (config.bundler === 'turbopack' && next.version === 14) { + scope.logger.error?.('Turbopack is not supported for Next.js v14. Falling back to webpack.'); + config.bundler = 'webpack'; + } + scope.logger.debug?.(`Detected Next.js version: ${next.version}`); if (config.prebuilt) {