From 866092bf54981b998d976e29ada53099a5b65455 Mon Sep 17 00:00:00 2001 From: Alice Miller Date: Fri, 17 Jul 2026 10:01:58 +1000 Subject: [PATCH] feat(plugin-nextjs): add opt-in Turbopack support Co-authored-by: Claude --- .changeset/calm-cats-enable-turbopack.md | 5 ++ packages/gasket-plugin-nextjs/README.md | 11 +-- packages/gasket-plugin-nextjs/lib/index.js | 2 + .../gasket-plugin-nextjs/lib/next-config.js | 30 ++++++++ .../gasket-plugin-nextjs/test/index.test.js | 1 + .../test/next-config.test.js | 72 +++++++++++++++++++ 6 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 .changeset/calm-cats-enable-turbopack.md create mode 100644 packages/gasket-plugin-nextjs/lib/next-config.js create mode 100644 packages/gasket-plugin-nextjs/test/next-config.test.js diff --git a/.changeset/calm-cats-enable-turbopack.md b/.changeset/calm-cats-enable-turbopack.md new file mode 100644 index 0000000000..194623a6e8 --- /dev/null +++ b/.changeset/calm-cats-enable-turbopack.md @@ -0,0 +1,5 @@ +--- +"@gasket/plugin-nextjs": minor +--- + +Add opt-in Turbopack support for Gasket plugin server dependencies. diff --git a/packages/gasket-plugin-nextjs/README.md b/packages/gasket-plugin-nextjs/README.md index 933a73c85a..38ea4e9a76 100644 --- a/packages/gasket-plugin-nextjs/README.md +++ b/packages/gasket-plugin-nextjs/README.md @@ -64,10 +64,13 @@ export default gasket.actions.getNextConfig(); ### Next.js 16 and the Webpack bundler This plugin requires **Next.js 16** or later and injects Webpack configuration -into your Next app. In Next.js 16, Turbopack is the default bundler, so Gasket -apps must use the `--webpack` flag when running the Next CLI. Generated apps -and scripts use `next build --webpack` and `next dev --webpack` by default. -Next.js 16 also requires **Node.js 20.9+**. +into your Next app. Generated apps and scripts use `next build --webpack` and +`next dev --webpack` by default. Apps can opt into Turbopack by setting +`TURBOPACK=1` and using the `--turbopack` Next CLI flag. In this mode, the +plugin removes its Webpack callback and externalizes loaded Gasket plugins for +the server bundle. App-specific Webpack aliases and fallbacks must still be +represented in the app's `turbopack` configuration. Next.js 16 also requires +**Node.js 20.9+**. For general Webpack configurations, it is recommended to use features of the Gasket [Webpack plugin], which will be merged into the Next.js configuration. diff --git a/packages/gasket-plugin-nextjs/lib/index.js b/packages/gasket-plugin-nextjs/lib/index.js index 762d7df918..5a9c77070d 100644 --- a/packages/gasket-plugin-nextjs/lib/index.js +++ b/packages/gasket-plugin-nextjs/lib/index.js @@ -7,6 +7,7 @@ import * as actions from './actions.js'; import { prompt } from './prompt.js'; import create from './create.js'; import { webpackConfig } from './webpack-config.js'; +import nextConfig from './next-config.js'; import express from './express.js'; import fastify from './fastify.js'; import workbox from './workbox.js'; @@ -23,6 +24,7 @@ const plugin = { hooks: { configure, webpackConfig, + nextConfig, prompt, create, express, diff --git a/packages/gasket-plugin-nextjs/lib/next-config.js b/packages/gasket-plugin-nextjs/lib/next-config.js new file mode 100644 index 0000000000..ed34e189bd --- /dev/null +++ b/packages/gasket-plugin-nextjs/lib/next-config.js @@ -0,0 +1,30 @@ +/// + +const GASKET_PLUGIN_NAME = /(^@gasket\/plugin-|^@[\w-]+\/gasket-plugin-)/; +// eslint-disable-next-line no-process-env +const processEnv = process.env; + +/** + * Configure Next.js for Turbopack when explicitly enabled. + * @type {import('@gasket/core').HookHandler<'nextConfig'>} + */ +export default function nextConfig(gasket, config) { + if (processEnv.TURBOPACK !== '1') return config; + + const turbopackConfig = { ...config }; + delete turbopackConfig.webpack; + + const pluginNames = (gasket.config.plugins ?? []).flatMap((plugin) => ( + typeof plugin?.name === 'string' && GASKET_PLUGIN_NAME.test(plugin.name) + ? [plugin.name] + : [] + )); + + turbopackConfig.serverExternalPackages = Array.from(new Set([ + ...(config.serverExternalPackages ?? []), + '@gasket/core', + ...pluginNames + ])); + + return turbopackConfig; +} diff --git a/packages/gasket-plugin-nextjs/test/index.test.js b/packages/gasket-plugin-nextjs/test/index.test.js index 2ec598b35e..cc009f53db 100644 --- a/packages/gasket-plugin-nextjs/test/index.test.js +++ b/packages/gasket-plugin-nextjs/test/index.test.js @@ -60,6 +60,7 @@ describe('Plugin', function () { 'express', 'fastify', 'metadata', + 'nextConfig', 'prompt', 'webpackConfig', 'workbox' diff --git a/packages/gasket-plugin-nextjs/test/next-config.test.js b/packages/gasket-plugin-nextjs/test/next-config.test.js new file mode 100644 index 0000000000..05c59128ca --- /dev/null +++ b/packages/gasket-plugin-nextjs/test/next-config.test.js @@ -0,0 +1,72 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import nextConfig from '../lib/next-config.js'; + +// eslint-disable-next-line no-process-env +const processEnv = process.env; +const originalTurbopack = processEnv.TURBOPACK; + +describe('nextConfig', function () { + afterEach(function () { + if (typeof originalTurbopack === 'undefined') { + delete processEnv.TURBOPACK; + } else { + processEnv.TURBOPACK = originalTurbopack; + } + }); + + it('returns the existing config when Turbopack is disabled', function () { + delete processEnv.TURBOPACK; + const config = { webpack: function webpack() {} }; + + expect(nextConfig({ config: {} }, config)).toBe(config); + }); + + it('removes webpack configuration under Turbopack', function () { + processEnv.TURBOPACK = '1'; + + const result = nextConfig({ config: {} }, { webpack: function webpack() {} }); + + expect(result).not.toHaveProperty('webpack'); + }); + + it('externalizes Gasket core and loaded plugins', function () { + processEnv.TURBOPACK = '1'; + const gasket = { + config: { + plugins: [ + { name: '@gasket/plugin-nextjs' }, + { name: '@godaddy/gasket-plugin-uxp' }, + { name: 'local-plugin' }, + {} + ] + } + }; + + const result = nextConfig(gasket, {}); + + expect(result.serverExternalPackages).toEqual([ + '@gasket/core', + '@gasket/plugin-nextjs', + '@godaddy/gasket-plugin-uxp' + ]); + }); + + it('preserves and deduplicates existing server externals', function () { + processEnv.TURBOPACK = '1'; + const gasket = { + config: { + plugins: [{ name: '@gasket/plugin-nextjs' }] + } + }; + + const result = nextConfig(gasket, { + serverExternalPackages: ['existing-package', '@gasket/core'] + }); + + expect(result.serverExternalPackages).toEqual([ + 'existing-package', + '@gasket/core', + '@gasket/plugin-nextjs' + ]); + }); +});