next.config.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. /**
  2. * == Notes for production build==
  3. * The modules required from this file must be transpiled before running `next build`.
  4. *
  5. * See: https://github.com/vercel/next.js/discussions/35969#discussioncomment-2522954
  6. */
  7. import type { NextConfig } from 'next';
  8. import {
  9. PHASE_PRODUCTION_BUILD,
  10. PHASE_PRODUCTION_SERVER,
  11. } from 'next/constants';
  12. import path from 'node:path';
  13. import bundleAnalyzer from '@next/bundle-analyzer';
  14. import nextI18nConfig from './config/next-i18next.config';
  15. import {
  16. createChunkModuleStatsPlugin,
  17. listPrefixedPackages,
  18. } from './src/utils/next.config.utils';
  19. const { i18n, localePath } = nextI18nConfig;
  20. const getTranspilePackages = (): string[] => {
  21. const packages = [
  22. // listing ESM packages until experimental.esmExternals works correctly to avoid ERR_REQUIRE_ESM
  23. 'react-markdown',
  24. 'unified',
  25. 'markdown-table',
  26. 'bail',
  27. 'ccount',
  28. 'character-entities',
  29. 'character-entities-html4',
  30. 'character-entities-legacy',
  31. 'comma-separated-tokens',
  32. 'decode-named-character-reference',
  33. 'devlop',
  34. 'fault',
  35. 'escape-string-regexp',
  36. 'hastscript',
  37. 'html-void-elements',
  38. 'is-absolute-url',
  39. 'is-plain-obj',
  40. 'longest-streak',
  41. 'micromark',
  42. 'property-information',
  43. 'space-separated-tokens',
  44. 'stringify-entities',
  45. 'trim-lines',
  46. 'trough',
  47. 'web-namespaces',
  48. 'vfile',
  49. 'vfile-location',
  50. 'vfile-message',
  51. 'zwitch',
  52. 'emoticon',
  53. 'direction', // for hast-util-select
  54. 'bcp-47-match', // for hast-util-select
  55. 'parse-entities',
  56. 'character-reference-invalid',
  57. 'is-hexadecimal',
  58. 'is-alphabetical',
  59. 'is-alphanumerical',
  60. 'github-slugger',
  61. 'html-url-attributes',
  62. 'estree-util-is-identifier-name',
  63. 'superjson',
  64. ...listPrefixedPackages([
  65. 'remark-',
  66. 'rehype-',
  67. 'hast-',
  68. 'mdast-',
  69. 'micromark-',
  70. 'unist-',
  71. ]),
  72. ];
  73. return packages;
  74. };
  75. const optimizePackageImports: string[] = [
  76. '@growi/core',
  77. '@growi/editor',
  78. '@growi/pluginkit',
  79. '@growi/presentation',
  80. '@growi/preset-themes',
  81. '@growi/remark-attachment-refs',
  82. '@growi/remark-drawio',
  83. '@growi/remark-growi-directive',
  84. '@growi/remark-lsx',
  85. '@growi/slack',
  86. '@growi/ui',
  87. ];
  88. export default (phase: string): NextConfig => {
  89. /** @type {import('next').NextConfig} */
  90. const nextConfig: NextConfig = {
  91. reactStrictMode: true,
  92. poweredByHeader: false,
  93. pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],
  94. i18n,
  95. serverExternalPackages: [
  96. 'handsontable', // Legacy v6.2.2 requires @babel/polyfill which is unavailable; client-only via dynamic import
  97. ],
  98. // for build
  99. typescript: {
  100. tsconfigPath: 'tsconfig.build.client.json',
  101. },
  102. transpilePackages:
  103. phase !== PHASE_PRODUCTION_SERVER ? getTranspilePackages() : undefined,
  104. sassOptions: {
  105. loadPaths: [path.resolve(__dirname, 'src')],
  106. },
  107. experimental: {
  108. optimizePackageImports,
  109. },
  110. turbopack: {
  111. rules: {
  112. // Server-only: auto-wrap getServerSideProps with SuperJSON serialization
  113. '*.page.ts': [
  114. {
  115. condition: { not: 'browser' },
  116. loaders: [
  117. path.resolve(__dirname, 'src/utils/superjson-ssr-loader.ts'),
  118. ],
  119. as: '*.ts',
  120. },
  121. ],
  122. '*.page.tsx': [
  123. {
  124. condition: { not: 'browser' },
  125. loaders: [
  126. path.resolve(__dirname, 'src/utils/superjson-ssr-loader.ts'),
  127. ],
  128. as: '*.tsx',
  129. },
  130. ],
  131. },
  132. resolveAlias: {
  133. // Exclude fs from client bundle
  134. fs: { browser: './src/lib/empty-module.ts' },
  135. // Exclude server-only packages from client bundle
  136. 'dtrace-provider': { browser: './src/lib/empty-module.ts' },
  137. mongoose: { browser: './src/lib/empty-module.ts' },
  138. 'mathjax-full': { browser: './src/lib/empty-module.ts' },
  139. 'i18next-fs-backend': { browser: './src/lib/empty-module.ts' },
  140. bunyan: { browser: './src/lib/empty-module.ts' },
  141. 'bunyan-format': { browser: './src/lib/empty-module.ts' },
  142. 'core-js': { browser: './src/lib/empty-module.ts' },
  143. },
  144. },
  145. webpack(config, options) {
  146. // Auto-wrap getServerSideProps with superjson serialization (replaces next-superjson SWC plugin)
  147. if (options.isServer) {
  148. config.module!.rules!.push({
  149. test: /\.page\.(tsx|ts)$/,
  150. use: [path.resolve(__dirname, 'src/utils/superjson-ssr-loader.ts')],
  151. });
  152. }
  153. if (!options.isServer) {
  154. // Avoid "Module not found: Can't resolve 'fs'"
  155. // See: https://stackoverflow.com/a/68511591
  156. config.resolve!.fallback = { ...config.resolve!.fallback, fs: false };
  157. // exclude packages from the output bundles
  158. config.module!.rules!.push(
  159. ...[
  160. /dtrace-provider/,
  161. /mongoose/,
  162. /mathjax-full/, // required from marp
  163. /i18next-fs-backend/, // server-only filesystem translation backend (leaks via next-i18next)
  164. /\/bunyan\//, // server-only logging (client uses browser-bunyan via universal-bunyan)
  165. /bunyan-format/, // server-only log formatter (client uses @browser-bunyan/console-formatted-stream)
  166. /[\\/]core-js[\\/]/, // polyfills baked into next-i18next/react-stickynode dist; all APIs natively supported by target browsers (Chrome 64+, Safari 12+)
  167. ].map((packageRegExp) => {
  168. return {
  169. test: packageRegExp,
  170. use: 'null-loader',
  171. };
  172. }),
  173. );
  174. }
  175. // extract sourcemap
  176. if (options.dev) {
  177. config.module!.rules!.push({
  178. test: /.(c|m)?js$/,
  179. exclude: [/node_modules/, path.resolve(__dirname)],
  180. enforce: 'pre',
  181. use: ['source-map-loader'],
  182. });
  183. }
  184. // setup i18next-hmr
  185. if (!options.isServer && options.dev) {
  186. const { I18NextHMRPlugin } = require('i18next-hmr/webpack');
  187. config.plugins!.push(new I18NextHMRPlugin({ localesDir: localePath }));
  188. }
  189. // Log eager vs lazy module counts for dev compilation analysis
  190. if (!options.isServer && options.dev) {
  191. // biome-ignore lint/suspicious/noExplicitAny: webpack plugin type compatibility
  192. config.plugins!.push(createChunkModuleStatsPlugin() as any);
  193. }
  194. return config;
  195. },
  196. };
  197. // production server — skip bundle analyzer
  198. if (phase === PHASE_PRODUCTION_SERVER) {
  199. return nextConfig;
  200. }
  201. const withBundleAnalyzer = bundleAnalyzer({
  202. enabled:
  203. phase === PHASE_PRODUCTION_BUILD &&
  204. (process.env.ANALYZE === 'true' || process.env.ANALYZE === '1'),
  205. });
  206. return withBundleAnalyzer(nextConfig);
  207. };