next.config.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. const path = require('path'); // path import
  2. const webpack = require('webpack');
  3. const { withExpo } = require('@expo/next-adapter')
  4. const merge = require('deepmerge');
  5. const { ImageRemotePatterns } = require('app/settings/images_allowlist');
  6. // Import and run resource copying
  7. const fs = require('fs');
  8. // Resource copy helper
  9. function copyCustomizationResources() {
  10. const customizationRoot = path.resolve(__dirname, '../../packages/app/customization');
  11. const resourcesSource = path.resolve(customizationRoot, 'resources/web');
  12. const staticDest = path.resolve(__dirname, 'public/static');
  13. // Create static folder
  14. if (!fs.existsSync(staticDest)) {
  15. fs.mkdirSync(staticDest, { recursive: true });
  16. }
  17. if (fs.existsSync(resourcesSource)) {
  18. const files = fs.readdirSync(resourcesSource);
  19. let copiedCount = 0;
  20. files.forEach(file => {
  21. if (file.endsWith('.bak')) return;
  22. const sourcePath = path.join(resourcesSource, file);
  23. const stat = fs.statSync(sourcePath);
  24. if (stat.isFile()) {
  25. // Copy ONLY into static/
  26. fs.copyFileSync(sourcePath, path.join(staticDest, file));
  27. copiedCount++;
  28. }
  29. });
  30. }
  31. // Client projects reference /static/* in metadata; copy repo defaults when
  32. // customization/resources/web does not supply overrides.
  33. const staticDefaults = [
  34. { src: path.resolve(__dirname, 'public/manifest.json'), dest: 'manifest.json' },
  35. { src: path.resolve(__dirname, 'public/favicon.svg'), dest: 'favicon.svg' },
  36. ];
  37. staticDefaults.forEach(({ src, dest }) => {
  38. const destPath = path.join(staticDest, dest);
  39. if (!fs.existsSync(destPath) && fs.existsSync(src)) {
  40. fs.copyFileSync(src, destPath);
  41. }
  42. });
  43. }
  44. // Run copy BEFORE loading config
  45. copyCustomizationResources();
  46. const nextConfigCustom = require('app/customization/config/next.config');
  47. /** @type {import('next').NextConfig} */
  48. // Monorepo path — check local node_modules first, then root
  49. const workspaceRoot = path.resolve(__dirname, '../..');
  50. const tenPlayWebviewLocalPath = path.resolve(__dirname, 'node_modules/@10play/react-native-web-webview');
  51. const tenPlayWebviewRootPath = path.resolve(workspaceRoot, 'node_modules/@10play/react-native-web-webview');
  52. const tenPlayWebviewPath = require('fs').existsSync(tenPlayWebviewLocalPath)
  53. ? path.resolve(tenPlayWebviewLocalPath, 'lib/module/index.js')
  54. : path.resolve(tenPlayWebviewRootPath, 'lib/module/index.js');
  55. const tenPlayWebviewShimPath = require('fs').existsSync(tenPlayWebviewLocalPath)
  56. ? path.resolve(tenPlayWebviewLocalPath, 'lib/module/shim.js')
  57. : path.resolve(tenPlayWebviewRootPath, 'lib/module/shim.js');
  58. const reanimatedPath = path.resolve(__dirname, 'node_modules/react-native-reanimated');
  59. // Реальный @tiptap/react (используется веб-сборкой react-native-enriched-html).
  60. // Подключается отдельным alias, чтобы шим '@tiptap/react' не вызывал сам себя.
  61. const tiptapReactLocalPath = path.resolve(__dirname, 'node_modules/@tiptap/react/dist/index.js');
  62. const tiptapReactRootPath = path.resolve(workspaceRoot, 'node_modules/@tiptap/react/dist/index.js');
  63. const tiptapReactRealPath = fs.existsSync(tiptapReactLocalPath)
  64. ? tiptapReactLocalPath
  65. : tiptapReactRootPath;
  66. const md4cEmscriptenLocalPath = path.resolve(
  67. __dirname,
  68. 'node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js'
  69. );
  70. const md4cEmscriptenRootPath = path.resolve(
  71. workspaceRoot,
  72. 'node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js'
  73. );
  74. const md4cEmscriptenSourcePath = fs.existsSync(md4cEmscriptenLocalPath)
  75. ? md4cEmscriptenLocalPath
  76. : md4cEmscriptenRootPath;
  77. // md4c.js — UMD/CJS Emscripten-бандл, но lib/module/package.json пакета содержит
  78. // {"type":"module"}, из-за чего webpack парсит его как ESM и теряет module.exports.
  79. // Копируем в .cjs вне scope пакета — webpack всегда собирает .cjs как CommonJS.
  80. const md4cEmscriptenRealPath = path.resolve(__dirname, 'shims/md4c-emscripten.cjs');
  81. if (fs.existsSync(md4cEmscriptenSourcePath)) {
  82. const srcStat = fs.statSync(md4cEmscriptenSourcePath);
  83. const needCopy =
  84. !fs.existsSync(md4cEmscriptenRealPath) ||
  85. fs.statSync(md4cEmscriptenRealPath).size !== srcStat.size;
  86. if (needCopy) {
  87. fs.copyFileSync(md4cEmscriptenSourcePath, md4cEmscriptenRealPath);
  88. }
  89. }
  90. const nextConfig = {
  91. typescript: {
  92. ignoreBuildErrors: true,
  93. },
  94. // Bridge non-public GOOGLE_WEB_CLIENT_ID into the client bundle so SSR and
  95. // hydration agree on whether Google auth (and the OR separator) render.
  96. // Prefer NEXT_PUBLIC_* when set; fall back to the server-only name used in .env.
  97. env: {
  98. NEXT_PUBLIC_GOOGLE_WEB_CLIENT_ID:
  99. process.env.NEXT_PUBLIC_GOOGLE_WEB_CLIENT_ID ||
  100. process.env.GOOGLE_WEB_CLIENT_ID ||
  101. '',
  102. },
  103. // Disable source maps in production
  104. productionBrowserSourceMaps: false,
  105. /*experimental: {
  106. ppr: true,
  107. },*/
  108. experimental: {
  109. staleTimes: {
  110. dynamic: 0,
  111. static: 180,
  112. },
  113. },
  114. // reanimated (and thus, Moti) doesn't work with strict mode currently...
  115. // https://github.com/nandorojo/moti/issues/224
  116. // https://github.com/necolas/react-native-web/pull/2330
  117. // https://github.com/nandorojo/moti/issues/224
  118. // once that gets fixed, set this back to true
  119. reactStrictMode: false,
  120. poweredByHeader: false,
  121. allowedDevOrigins: ['yasko.local', 'neo.localhost', 'bs-local.com'],
  122. /*experimental: {
  123. forceSwcTransforms: true,
  124. // scrollRestoration: true,
  125. swcPlugins: [[require.resolve('./plugins/swc_plugin_reanimated.wasm')]],
  126. },*/
  127. transpilePackages: [
  128. 'react-native',
  129. 'react-native-web',
  130. 'solito',
  131. '@expo/html-elements',
  132. 'react-native-gesture-handler',
  133. '@react-native-clipboard/clipboard',
  134. '@babel/core',
  135. '@react-navigation/native',
  136. 'react-native-calendars',
  137. 'react-native-image-pan-zoom',
  138. 'react-native-swipe-gestures',
  139. 'react-native-toast-message',
  140. 'expo-haptics',
  141. 'expo-modules-core',
  142. 'recyclerlistview',
  143. 'expo-crypto',
  144. 'jotai',
  145. '@react-native-picker/picker',
  146. '@10play/tentap-editor',
  147. '@10play/react-native-web-webview',
  148. '@appandflow/react-native-google-autocomplete',
  149. 'semver',
  150. 'react-native-webview',
  151. 'expo',
  152. 'expo-audio',
  153. 'expo-image-picker',
  154. 'expo-location',
  155. 'expo-camera',
  156. 'expo-document-picker',
  157. 'expo-image-manipulator',
  158. 'react-native-enriched-markdown',
  159. '@mdxeditor/editor',
  160. 'expo-constants',
  161. "react-native-svg",
  162. '@expo/metro-runtime',
  163. 'i18next',
  164. 'react-i18next',
  165. 'react-native-enriched-html',
  166. 'react-native-localize',
  167. 'victory-native',
  168. '@stripe/stripe-react-native',
  169. 'react-native-star-rating-widget',
  170. '@openspacelabs/react-native-zoomable-view',
  171. 'lucide-react-native',
  172. 'web-haptics',
  173. ],
  174. webpack: (config, { isServer }) => {
  175. // Add aliases
  176. config.resolve.alias = {
  177. ...config.resolve.alias,
  178. 'react-native': 'react-native-web',
  179. 'react-native-webview': tenPlayWebviewPath,
  180. 'react-native-webview$': tenPlayWebviewPath,
  181. 'jotai': path.resolve(workspaceRoot, 'node_modules/jotai'),
  182. 'crypto': 'expo-crypto',
  183. 'react-native-svg': path.resolve(__dirname, 'node_modules/react-native-svg'),
  184. 'react-native/Libraries/Utilities/codegenNativeComponent': tenPlayWebviewShimPath,
  185. 'react-native/Libraries/Utilities/codegenNativeComponent$': tenPlayWebviewShimPath,
  186. 'react-native-reanimated': reanimatedPath, // <-- Pin version 3.10.1
  187. 'react-native-localize': path.resolve(__dirname, 'stubs/react-native-localize.js'),
  188. // Шим форсит immediatelyRender:false (см. shims/tiptap-react.js).
  189. // '$' — точное совпадение, поэтому '@tiptap/react/menus' и пр. не трогаем.
  190. '@tiptap/react$': path.resolve(__dirname, 'shims/tiptap-react.js'),
  191. '@tiptap-react-real$': tiptapReactRealPath,
  192. 'md4c-emscripten-real$': md4cEmscriptenRealPath,
  193. };
  194. // Add fallback for codegenNativeComponent
  195. config.resolve.fallback = {
  196. ...config.resolve.fallback,
  197. 'react-native/Libraries/Utilities/codegenNativeComponent': tenPlayWebviewShimPath,
  198. };
  199. // Put local node_modules FIRST in resolve.modules
  200. config.resolve.modules = [
  201. path.resolve(__dirname, 'node_modules'), // <-- FIRST: local 3.10.1
  202. ...(config.resolve.modules || []).filter(m =>
  203. m !== path.resolve(__dirname, 'node_modules') &&
  204. m !== path.resolve(workspaceRoot, 'node_modules')
  205. ),
  206. path.resolve(workspaceRoot, 'node_modules'), // <-- Then root (may contain 4.1.3)
  207. ];
  208. // Force module replacement via NormalModuleReplacementPlugin
  209. config.plugins = config.plugins || [];
  210. config.plugins.push(
  211. new webpack.DefinePlugin({
  212. 'process.env.EXPO_OS': JSON.stringify('web'),
  213. }),
  214. new webpack.NormalModuleReplacementPlugin(
  215. /^react-native-webview$/,
  216. (resource) => {
  217. // Replace with web build for all imports (especially from @10play/tentap-editor)
  218. resource.request = tenPlayWebviewPath;
  219. }
  220. ),
  221. // Force react-native-reanimated to version 3.10.1
  222. new webpack.NormalModuleReplacementPlugin(
  223. /^react-native-reanimated$/,
  224. (resource) => {
  225. resource.request = reanimatedPath;
  226. }
  227. ),
  228. // Fix Emscripten CJS default export for react-native-enriched-markdown WASM parser.
  229. // parseMarkdown uses dynamic import('./wasm/md4c') — request is relative, not full path.
  230. new webpack.NormalModuleReplacementPlugin(
  231. /^\.\/wasm\/md4c$/,
  232. path.resolve(__dirname, 'shims/md4c.js')
  233. )
  234. );
  235. config.module.rules.push({
  236. test: /\.(mp3|wav)$/,
  237. type: 'asset/resource',
  238. });
  239. return config;
  240. },
  241. images: {
  242. remotePatterns: ImageRemotePatterns,
  243. disableStaticImages: false,
  244. // Keep optimized variants warm so browse grids don't re-hit the optimizer on every visit.
  245. minimumCacheTTL: 60 * 60 * 24,
  246. // Card thumbnails never need 3840w; dropping the top sizes avoids expensive optimizer failures.
  247. deviceSizes: [640, 750, 828, 1080, 1200, 1920],
  248. imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  249. },
  250. }
  251. const withBundleAnalyzer = require('@next/bundle-analyzer')({
  252. enabled: process.env.ANALYZE === 'true',
  253. })
  254. module.exports = withExpo(withBundleAnalyzer(
  255. merge(nextConfig, nextConfigCustom)
  256. ))