Configure rsbuild for web build and fix compatibility issues
- Add rsbuild.config.ts with proper configuration for: - React, Babel, and Expo polyfills - Environment variable definitions for expo-constants and @atproto - Module aliases for react-native-web and ESM packages - Disabled vendor chunk splitting to fix CommonJS scope issues - Built-in bundle analyzer instead of webpack-bundle-analyzer - Sentry integration support - Patch react-native-uitextview with Text.web.tsx fallback - Exclude @atproto, tlds, and @braintree from Babel processing This completes the migration from @expo/webpack-config to Rsbuild. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import {createRequire} from 'node:module'
|
||||
import {dirname, join} from 'node:path'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
|
||||
import {defineConfig} from '@rsbuild/core'
|
||||
import {pluginBabel} from '@rsbuild/plugin-babel'
|
||||
import {pluginReact} from '@rsbuild/plugin-react'
|
||||
// @ts-expect-error - webpack plugins, types may not match exactly
|
||||
import {sentryWebpackPlugin} from '@sentry/webpack-plugin'
|
||||
|
||||
const {version} = require('./package.json')
|
||||
|
||||
const require_ = createRequire(import.meta.url)
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const nodeModules = join(__dirname, 'node_modules')
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
pluginReact(),
|
||||
pluginBabel({
|
||||
include: /\.(?:js|jsx|ts|tsx)$/,
|
||||
// Exclude already-compiled packages that don't need babel transformation
|
||||
exclude: [
|
||||
/node_modules\/@atproto/,
|
||||
/node_modules\/tlds/,
|
||||
/node_modules\/@braintree/,
|
||||
],
|
||||
babelLoaderOptions: {
|
||||
presets: [['babel-preset-expo', {lazyImports: true}]],
|
||||
plugins: [
|
||||
'macros',
|
||||
['babel-plugin-react-compiler', {target: '19'}],
|
||||
[
|
||||
'module:react-native-dotenv',
|
||||
{
|
||||
envName: 'APP_ENV',
|
||||
moduleName: '@env',
|
||||
path: '.env',
|
||||
allowUndefined: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'module-resolver',
|
||||
{alias: {'#': './src', crypto: './src/platform/crypto.ts'}},
|
||||
],
|
||||
'react-native-reanimated/plugin',
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
source: {
|
||||
entry: {index: './index.web.js'},
|
||||
define: {
|
||||
__DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
|
||||
// Expo public env vars - these are accessed via process.env.EXPO_PUBLIC_*
|
||||
'process.env.EXPO_PUBLIC_ENV': JSON.stringify(
|
||||
process.env.EXPO_PUBLIC_ENV,
|
||||
),
|
||||
'process.env.EXPO_PUBLIC_RELEASE_VERSION': JSON.stringify(
|
||||
process.env.EXPO_PUBLIC_RELEASE_VERSION,
|
||||
),
|
||||
'process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER': JSON.stringify(
|
||||
process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER,
|
||||
),
|
||||
'process.env.EXPO_PUBLIC_BUNDLE_DATE': JSON.stringify(
|
||||
process.env.EXPO_PUBLIC_BUNDLE_DATE,
|
||||
),
|
||||
'process.env.EXPO_PUBLIC_LOG_LEVEL': JSON.stringify(
|
||||
process.env.EXPO_PUBLIC_LOG_LEVEL,
|
||||
),
|
||||
'process.env.EXPO_PUBLIC_LOG_DEBUG': JSON.stringify(
|
||||
process.env.EXPO_PUBLIC_LOG_DEBUG,
|
||||
),
|
||||
'process.env.EXPO_PUBLIC_BLUESKY_PROXY_DID': JSON.stringify(
|
||||
process.env.EXPO_PUBLIC_BLUESKY_PROXY_DID,
|
||||
),
|
||||
'process.env.EXPO_PUBLIC_CHAT_PROXY_DID': JSON.stringify(
|
||||
process.env.EXPO_PUBLIC_CHAT_PROXY_DID,
|
||||
),
|
||||
'process.env.EXPO_PUBLIC_SENTRY_DSN': JSON.stringify(
|
||||
process.env.EXPO_PUBLIC_SENTRY_DSN,
|
||||
),
|
||||
'process.env.EXPO_PUBLIC_BITDRIFT_API_KEY': JSON.stringify(
|
||||
process.env.EXPO_PUBLIC_BITDRIFT_API_KEY,
|
||||
),
|
||||
'process.env.EXPO_PUBLIC_GCP_PROJECT_ID': JSON.stringify(
|
||||
process.env.EXPO_PUBLIC_GCP_PROJECT_ID,
|
||||
),
|
||||
// Other env vars used in the app
|
||||
'process.env.BAPP_CONFIG_DEV_URL': JSON.stringify(
|
||||
process.env.BAPP_CONFIG_DEV_URL,
|
||||
),
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
'process.env.JEST_WORKER_ID': JSON.stringify(process.env.JEST_WORKER_ID),
|
||||
// expo-constants uses this for web manifest
|
||||
'process.env.APP_MANIFEST': JSON.stringify({}),
|
||||
// expo uses these for platform detection
|
||||
'process.env.EXPO_OS': JSON.stringify('web'),
|
||||
'process.env.EXPO_BASE_URL': JSON.stringify(''),
|
||||
},
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: [
|
||||
'.web.tsx',
|
||||
'.web.ts',
|
||||
'.web.js',
|
||||
'.tsx',
|
||||
'.ts',
|
||||
'.js',
|
||||
'.json',
|
||||
],
|
||||
alias: {
|
||||
'react-native$': 'react-native-web',
|
||||
'react-native-webview': 'react-native-web-webview',
|
||||
'react-native-gesture-handler': false,
|
||||
// Use source to allow .web.tsx resolution (patched with web fallback)
|
||||
'react-native-uitextview': join(
|
||||
nodeModules,
|
||||
'react-native-uitextview/src/index',
|
||||
),
|
||||
// Force ESM version (same as webpack.config.js)
|
||||
'unicode-segmenter/grapheme': require_
|
||||
.resolve('unicode-segmenter/grapheme')
|
||||
.replace(/\.cjs$/, '.js'),
|
||||
// multiformats package exports resolution (same as metro.config.js)
|
||||
// Use $ suffix for exact match and direct file paths
|
||||
'multiformats/hashes/hasher$': join(
|
||||
nodeModules,
|
||||
'multiformats/esm/src/hashes/hasher.js',
|
||||
),
|
||||
'multiformats/cid$': join(nodeModules, 'multiformats/esm/src/cid.js'),
|
||||
// @ipld/dag-cbor only exports '.' in package.json, use direct file path
|
||||
'@ipld/dag-cbor$': join(nodeModules, '@ipld/dag-cbor/src/index.js'),
|
||||
},
|
||||
},
|
||||
|
||||
output: {
|
||||
distPath: {
|
||||
root: 'web-build',
|
||||
js: 'static/js',
|
||||
css: 'static/css',
|
||||
image: 'static/media',
|
||||
font: 'static/media',
|
||||
svg: 'static/media',
|
||||
media: 'static/media',
|
||||
},
|
||||
filename: {
|
||||
js: '[name].[contenthash:8].js',
|
||||
css: '[name].[contenthash:8].css',
|
||||
},
|
||||
assetPrefix: process.env.NODE_ENV === 'production' ? 'auto' : '/',
|
||||
manifest: true,
|
||||
sourceMap: {
|
||||
js:
|
||||
process.env.GENERATE_SOURCEMAP === 'false'
|
||||
? false
|
||||
: process.env.NODE_ENV === 'production'
|
||||
? 'source-map'
|
||||
: 'cheap-module-source-map',
|
||||
css: true,
|
||||
},
|
||||
},
|
||||
|
||||
html: {
|
||||
template: './web/index.html',
|
||||
templateParameters: {WEB_TITLE: 'Bluesky'},
|
||||
favicon: './assets/favicon.png',
|
||||
},
|
||||
|
||||
server: {port: 19006},
|
||||
|
||||
performance: {
|
||||
// Enable with BUNDLE_ANALYZE=true rsbuild build
|
||||
// Or BUNDLE_ANALYZE=server to open in browser
|
||||
bundleAnalyze: process.env.BUNDLE_ANALYZE
|
||||
? {
|
||||
analyzerMode:
|
||||
process.env.BUNDLE_ANALYZE === 'server' ? 'server' : 'static',
|
||||
openAnalyzer: process.env.BUNDLE_ANALYZE === 'server',
|
||||
generateStatsFile: true,
|
||||
statsFilename: 'stats.json',
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
|
||||
tools: {
|
||||
rspack: (config, {rspack}) => {
|
||||
// Wrap chunks in IIFE for proper CommonJS scope (fixes "exports is not defined")
|
||||
config.output = config.output || {}
|
||||
config.output.iife = true
|
||||
|
||||
// Disable async vendor chunk splitting - fixes "exports is not defined"
|
||||
// error with CommonJS modules like @atproto/api
|
||||
config.optimization = config.optimization || {}
|
||||
config.optimization.splitChunks = {
|
||||
chunks: 'async',
|
||||
cacheGroups: {
|
||||
// Disable default vendor chunk splitting
|
||||
defaultVendors: false,
|
||||
default: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Module replacements for packages that don't work with rspack's module resolution
|
||||
config.plugins = config.plugins || []
|
||||
|
||||
// multiformats/hashes/hasher - redirect to ESM source file
|
||||
config.plugins.push(
|
||||
new rspack.NormalModuleReplacementPlugin(
|
||||
/^multiformats\/hashes\/hasher$/,
|
||||
join(nodeModules, 'multiformats/esm/src/hashes/hasher.js'),
|
||||
),
|
||||
)
|
||||
// multiformats/cid - redirect to ESM source file
|
||||
config.plugins.push(
|
||||
new rspack.NormalModuleReplacementPlugin(
|
||||
/^multiformats\/cid$/,
|
||||
join(nodeModules, 'multiformats/esm/src/cid.js'),
|
||||
),
|
||||
)
|
||||
// @ipld/dag-cbor - redirect to src
|
||||
config.plugins.push(
|
||||
new rspack.NormalModuleReplacementPlugin(
|
||||
/^@ipld\/dag-cbor$/,
|
||||
join(nodeModules, '@ipld/dag-cbor/src/index.js'),
|
||||
),
|
||||
)
|
||||
|
||||
// Force CommonJS modules to be parsed correctly
|
||||
config.module = config.module || {}
|
||||
config.module.parser = config.module.parser || {}
|
||||
config.module.parser.javascript = config.module.parser.javascript || {}
|
||||
config.module.parser.javascript.commonjsMagicComments = true
|
||||
|
||||
config.module.rules = config.module.rules || []
|
||||
|
||||
// postMock.html for react-native-web-webview
|
||||
config.module.rules.push({
|
||||
test: /postMock\.html$/,
|
||||
type: 'asset/resource',
|
||||
generator: {filename: '[name][ext]'},
|
||||
})
|
||||
|
||||
// Sentry (optional)
|
||||
if (process.env.SENTRY_AUTH_TOKEN) {
|
||||
config.plugins = config.plugins || []
|
||||
config.plugins.push(
|
||||
sentryWebpackPlugin({
|
||||
org: 'blueskyweb',
|
||||
project: 'app',
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
release: {
|
||||
name: process.env.SENTRY_RELEASE || version,
|
||||
dist: process.env.SENTRY_DIST,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user