Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at cope-settings-sync 452 lines 15 kB view raw
1import path from 'node:path' 2import rspack from '@rspack/core' 3import {RspackManifestPlugin} from 'rspack-manifest-plugin' 4import {sentryWebpackPlugin} from '@sentry/webpack-plugin' 5import {version} from './package.json' 6import {existsSync, readdirSync, symlink} from 'node:fs' 7 8const GENERATE_STATS = process.env.GENERATE_STATS === '1' 9const isProduction = process.env.NODE_ENV === 'production' 10 11// Collect all EXPO_PUBLIC_* env vars so they're available at build time, 12// mirroring what @expo/webpack-config does automatically. 13const expoPublicEnv = Object.fromEntries( 14 Object.entries(process.env) 15 .filter(([key]) => key.startsWith('EXPO_PUBLIC_')) 16 .map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]), 17) 18 19// Packages in node_modules that ship untranspiled JSX/Flow/modern syntax 20// and need to be run through SWC. 21const TRANSPILE_MODULES = { 22 prefixes: [ 23 'react-native', 24 'react-native-web', 25 'expo', 26 'unimodules', 27 'react-navigation', 28 ], 29 scopes: [ 30 '@react-native', 31 '@react-native-community', 32 '@expo', 33 '@unimodules', 34 '@bsky.app', 35 '@discord', 36 '@react-navigation', 37 ], 38 packages: [ 39 'native-base', 40 'normalize-url', 41 '@sentry/react-native', 42 'sentry-expo', 43 'bcp-47-match', 44 'nanoid', 45 ], 46} 47 48function getTranspileModuleDirs({ 49 prefixes, 50 scopes, 51 packages, 52}: typeof TRANSPILE_MODULES) { 53 const nodeModulesDir = path.resolve(__dirname, 'node_modules') 54 const dirs = new Set<string>() 55 56 const readDirNames = (dir: string) => { 57 if (!existsSync(dir)) return [] 58 return readdirSync(dir, {withFileTypes: true}) 59 .filter(entry => entry.isDirectory()) 60 .map(entry => entry.name) 61 } 62 63 for (const entry of readDirNames(nodeModulesDir)) { 64 console.log('Checking node_modules entry:', entry) 65 if ( 66 prefixes.some( 67 prefix => entry === prefix || entry.startsWith(`${prefix}-`), 68 ) 69 ) { 70 dirs.add(path.join(nodeModulesDir, entry)) 71 } 72 } 73 74 for (const scope of scopes) { 75 const scopeDir = path.join(nodeModulesDir, scope) 76 for (const entry of readDirNames(scopeDir)) { 77 dirs.add(path.join(scopeDir, entry)) 78 } 79 } 80 81 for (const pkg of packages) { 82 const pkgDir = path.join(nodeModulesDir, ...pkg.split('/')) 83 if (existsSync(pkgDir)) { 84 dirs.add(pkgDir) 85 } 86 } 87 88 return [...dirs] 89} 90 91const transpileModuleDirs = getTranspileModuleDirs(TRANSPILE_MODULES) 92const REANIMATED_BABEL_MODULES = ['react-native-keyboard-controller'] 93const reanimatedBabelModuleDirs = REANIMATED_BABEL_MODULES.map(pkg => 94 path.resolve(__dirname, 'node_modules', ...pkg.split('/')), 95).filter(existsSync) 96const SWC_TRANSPILE_EXCLUDE = /node_modules[\\/]react-native-uuid[\\/]/ 97const REANIMATED_BABEL_EXCLUDE = 98 /node_modules[\\/]react-native-keyboard-controller[\\/]/ 99 100/** @type {import('@rspack/core').Configuration} */ 101module.exports = { 102 mode: isProduction ? 'production' : 'development', 103 // Avoid eval-based sourcemaps in development. Firefox resolves relative 104 // sourcemap URLs from injected devtools scripts like `installHook.js.map` 105 // against an `<anonymous code>` URL when the bundle is eval-backed, which 106 // produces noisy 404s in the console. 107 devtool: isProduction ? 'source-map' : 'cheap-module-source-map', 108 109 entry: { 110 main: path.resolve(__dirname, 'index.web.js'), 111 }, 112 113 output: { 114 path: path.resolve(__dirname, 'web-build'), 115 filename: isProduction 116 ? 'static/js/[name].[contenthash:8].js' 117 : 'static/js/[name].js', 118 chunkFilename: isProduction 119 ? 'static/js/[name].[contenthash:8].chunk.js' 120 : 'static/js/[name].chunk.js', 121 assetModuleFilename: 'static/media/[name].[hash:8][ext]', 122 publicPath: isProduction ? 'auto' : '/', 123 clean: true, 124 }, 125 126 resolve: { 127 extensions: [ 128 '.web.tsx', 129 '.web.ts', 130 '.web.js', 131 '.web.jsx', 132 '.tsx', 133 '.ts', 134 '.jsx', 135 '.js', 136 '.json', 137 ], 138 alias: { 139 // Path alias for src/ 140 '#': path.resolve(__dirname, 'src'), 141 // React Native Web 142 'react-native$': 'react-native-web', 143 // Internal RN module mappings for compatibility 144 'react-native/Libraries/Components/View/ViewStylePropTypes$': 145 'react-native-web/dist/exports/View/ViewStylePropTypes', 146 'react-native/Libraries/EventEmitter/RCTDeviceEventEmitter$': 147 'react-native-web/dist/vendor/react-native/NativeEventEmitter/RCTDeviceEventEmitter', 148 'react-native/Libraries/vendor/emitter/EventEmitter$': 149 'react-native-web/dist/vendor/react-native/emitter/EventEmitter', 150 'react-native/Libraries/EventEmitter/NativeEventEmitter$': 151 'react-native-web/dist/vendor/react-native/NativeEventEmitter', 152 // Webview shim 153 'react-native-webview': 'react-native-web-webview', 154 // Crypto shim for expo-modules-core 155 crypto: path.resolve(__dirname, 'src/platform/crypto.ts'), 156 // Force ESM version of unicode-segmenter 157 'unicode-segmenter/grapheme': require 158 .resolve('unicode-segmenter/grapheme') 159 .replace(/\.cjs$/, '.js'), 160 // Block packages that should not load on web 161 'react-native-gesture-handler': false, 162 '@sentry-internal/replay': false, 163 }, 164 mainFields: ['browser', 'module', 'main'], 165 // Allow importing without file extensions in ESM packages 166 fullySpecified: false, 167 symlinks: false, // Don't resolve symlinks to support pnpm's node_modules structure 168 }, 169 170 module: { 171 rules: [ 172 // Disable fullySpecified for ESM packages that import without extensions 173 // (e.g. react-navigation importing react-native-web/dist/exports/Platform) 174 { 175 test: /\.m?js$/, 176 resolve: { 177 fullySpecified: false, 178 }, 179 }, 180 // Source files: use babel-loader for lingui macros, react-compiler, etc. 181 { 182 test: /\.[jt]sx?$/, 183 exclude: /node_modules/, 184 use: { 185 loader: 'babel-loader', 186 options: { 187 configFile: false, // Don't look for a babel.config.json to avoid conflicts with the one in the root of the monorepo. 188 babelrc: false, 189 cacheDirectory: true, 190 cacheCompression: false, // let rspack handle it 191 sourceType: 'unambiguous', 192 // based on babel.config.js but optimized for web and rspack 193 presets: [ 194 [ 195 'babel-preset-expo', 196 { 197 lazyImports: true, 198 native: { 199 // Disable ESM -> CJS compilation because rspack handles it. 200 disableImportExportTransform: true, 201 }, 202 }, 203 ], 204 ], 205 plugins: [ 206 '@lingui/babel-plugin-lingui-macro', 207 ['babel-plugin-react-compiler', {target: '19'}], 208 // omitted: react-native-dotenv (we use DefinePlugin instead) 209 // omitted: module-resolver (we use rspack's built-in aliasing instead) 210 'react-native-reanimated/plugin', // NOTE: this plugin MUST be last 211 ], 212 env: { 213 production: { 214 plugins: [], // omitted: transform-remove-console 215 }, 216 }, 217 }, 218 }, 219 }, 220 // Some published packages ship raw `"worklet"` functions and must go 221 // through Babel with the Reanimated plugin on web, matching Expo's 222 // webpack pipeline. SWC alone leaves those functions unworkletized. 223 { 224 test: /\.[jt]sx?$/, 225 include: reanimatedBabelModuleDirs, 226 use: { 227 loader: 'babel-loader', 228 options: { 229 configFile: false, 230 babelrc: false, 231 cacheDirectory: true, 232 cacheCompression: false, 233 sourceType: 'unambiguous', 234 presets: [ 235 [ 236 'babel-preset-expo', 237 { 238 lazyImports: true, 239 native: { 240 disableImportExportTransform: true, 241 }, 242 }, 243 ], 244 ], 245 plugins: ['react-native-reanimated/plugin'], 246 }, 247 }, 248 }, 249 // node_modules that ship untranspiled JSX/Flow: use rspack's builtin 250 // SWC loader which is much faster than babel for simple transforms. 251 { 252 test: /\.jsx?$/, 253 include: transpileModuleDirs, 254 exclude: [SWC_TRANSPILE_EXCLUDE, REANIMATED_BABEL_EXCLUDE], 255 use: { 256 loader: 'swc-loader', // rspack swc-loader doesn't support flow yet 257 options: { 258 jsc: { 259 parser: { 260 syntax: 'flow', 261 jsx: true, 262 }, 263 transform: { 264 react: { 265 runtime: 'automatic', 266 }, 267 }, 268 }, 269 }, 270 }, 271 }, 272 { 273 test: /\.tsx?$/, 274 include: transpileModuleDirs, 275 exclude: [SWC_TRANSPILE_EXCLUDE, REANIMATED_BABEL_EXCLUDE], 276 use: { 277 loader: 'swc-loader', 278 options: { 279 jsc: { 280 parser: { 281 syntax: 'typescript', 282 jsx: true, 283 }, 284 transform: { 285 react: { 286 runtime: 'automatic', 287 }, 288 }, 289 }, 290 }, 291 }, 292 }, 293 // HTML file loader for react-native-web-webview's postMock.html 294 { 295 test: /postMock\.html$/, 296 type: 'asset/resource', 297 generator: { 298 filename: 'static/[name][ext]', 299 }, 300 }, 301 // CSS support — imported from JS/TS files 302 { 303 test: /\.css$/, 304 type: 'css/auto', 305 }, 306 // Image assets 307 { 308 test: /\.(bmp|gif|jpe?g|png|svg|avif|webp)$/i, 309 type: 'asset', 310 parser: { 311 dataUrlCondition: { 312 maxSize: 8 * 1024, // 8KB 313 }, 314 }, 315 }, 316 // Font assets 317 { 318 test: /\.(woff|woff2|otf|ttf|eot)$/i, 319 type: 'asset/resource', 320 }, 321 ], 322 }, 323 324 plugins: [ 325 new rspack.HtmlRspackPlugin({ 326 template: path.resolve(__dirname, 'web/index.html'), 327 inject: true, 328 }), 329 new rspack.CopyRspackPlugin({ 330 patterns: [ 331 // Serve fonts at /static/fonts/ with stable names 332 {from: 'web/static/fonts', to: 'static/fonts'}, 333 // Serve the global stylesheet 334 {from: 'src/style.css', to: 'static/style.css'}, 335 ], 336 }), 337 new rspack.DefinePlugin({ 338 __DEV__: JSON.stringify(!isProduction), 339 'process.env.NODE_ENV': JSON.stringify( 340 isProduction ? 'production' : 'development', 341 ), 342 'process.env.JEST_WORKER_ID': JSON.stringify(undefined), 343 'process.env.LIVE_EVENTS_DEV_URL': JSON.stringify( 344 process.env.LIVE_EVENTS_DEV_URL || '', 345 ), 346 'process.env.APP_CONFIG_DEV_URL': JSON.stringify( 347 process.env.APP_CONFIG_DEV_URL || '', 348 ), 349 // provide sensible defaults for env vars that the web build expects but aren't defined in the environment 350 'process.env.EXPO_PUBLIC_ENV': JSON.stringify( 351 isProduction ? 'production' : 'development', 352 ), 353 'process.env.EAS_BUILD_PLATFORM': JSON.stringify('web'), 354 'process.env.SENTRY_AUTH_TOKEN': 'undefined', 355 'process.env.EXPO_PUBLIC_RELEASE_VERSION': 'undefined', 356 'process.env.EXPO_PUBLIC_LOG_LEVEL': '"debug"', 357 'process.env.EXPO_PUBLIC_LOG_DEBUG': '"*"', 358 'process.env.EXPO_PUBLIC_OAUTH_BASE_URL': 'undefined', 359 'process.env.EXPO_PUBLIC_OAUTH_CLIENT_NAME': 'undefined', 360 'process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER': 'undefined', 361 'process.env.EXPO_PUBLIC_BUNDLE_DATE': 'undefined', 362 'process.env.EXPO_PUBLIC_SENTRY_DSN': 'undefined', 363 'process.env.EXPO_PUBLIC_BLUESKY_PROXY_DID': 'undefined', 364 'process.env.EXPO_PUBLIC_CHAT_PROXY_DID': 'undefined', 365 'process.env.EXPO_PUBLIC_METRICS_API_HOST': 'undefined', 366 'process.env.EXPO_PUBLIC_GROWTHBOOK_API_HOST': 'undefined', 367 'process.env.EXPO_PUBLIC_GROWTHBOOK_CLIENT_KEY': 'undefined', 368 'process.env.EXPO_PUBLIC_BITDRIFT_API_KEY': 'undefined', 369 'process.env.EXPO_PUBLIC_GCP_PROJECT_ID': 'undefined', 370 'process.env.EXPO_PUBLIC_PUBLIC_BSKY_SERVICE': 'undefined', 371 'process.env.EXPO_PUBLIC_APPVIEW_DID_PROXY': 'undefined', 372 'process.env.APP_MANIFEST': 'undefined', 373 'process.env.__SENTRY_METRO_DEV_SERVER__': 'undefined', 374 'process.env.EXPO_OS': JSON.stringify('web'), 375 // Inject all EXPO_PUBLIC_* env vars 376 ...expoPublicEnv, 377 }), 378 // Generate asset-manifest.json matching the format the Go server expects. 379 // The post-web-build script reads `entrypoints` from this manifest. 380 new RspackManifestPlugin({ 381 fileName: 'asset-manifest.json', 382 generate: (seed, files, entrypoints) => { 383 const entrypointFiles = entrypoints.main || [] 384 return { 385 files: files.reduce((manifest, file) => { 386 manifest[file.name] = file.path 387 return manifest 388 }, seed), 389 entrypoints: entrypointFiles.filter(f => !f.endsWith('.map')), 390 } 391 }, 392 }), 393 // Sentry source maps 394 isProduction && 395 process.env.SENTRY_AUTH_TOKEN && 396 sentryWebpackPlugin({ 397 org: 'blueskyweb', 398 project: 'app', 399 authToken: process.env.SENTRY_AUTH_TOKEN, 400 release: { 401 name: process.env.SENTRY_RELEASE || version, 402 dist: process.env.SENTRY_DIST, 403 }, 404 }), 405 ].filter(Boolean), 406 407 optimization: { 408 splitChunks: { 409 chunks: 'all', 410 cacheGroups: { 411 framework: { 412 test: /[\\/]node_modules[\\/](react|react-dom|react-native-web|@react-navigation|expo|@expo)[\\/]/, 413 name: 'framework', 414 chunks: 'initial', 415 priority: 20, 416 reuseExistingChunk: true, 417 }, 418 vendor: { 419 test: /[\\/]node_modules[\\/]/, 420 name: 'vendor', 421 chunks: 'initial', 422 priority: -10, 423 reuseExistingChunk: true, 424 }, 425 }, 426 }, 427 runtimeChunk: 'single', 428 minimize: isProduction, 429 }, 430 431 devServer: { 432 static: { 433 directory: path.resolve(__dirname, 'web'), 434 }, 435 port: 19006, 436 hot: true, 437 historyApiFallback: true, 438 compress: true, 439 }, 440 441 // Don't bundle node built-ins (shouldn't be needed on web) 442 externalsPresets: {node: false}, 443 node: { 444 __filename: false, 445 }, 446 447 stats: GENERATE_STATS ? 'verbose' : 'normal', 448 449 experiments: { 450 css: true, 451 }, 452}