diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go
index eb59adf40f..f2dd0d1f86 100644
--- a/bskyweb/cmd/bskyweb/server.go
+++ b/bskyweb/cmd/bskyweb/server.go
@@ -280,8 +280,8 @@ func serve(cctx *cli.Context) error {
path := c.Request().URL.Path
maxAge := 1 * (60 * 60) // default is 1 hour
- // all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time
- if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") {
+ // all assets in /static/_expo, /static/assets, /static/media are content-hashed and can be cached for a long time
+ if strings.HasPrefix(path, "/static/_expo/") || strings.HasPrefix(path, "/static/assets/") || strings.HasPrefix(path, "/static/media/") {
maxAge = 365 * (60 * 60 * 24) // 1 year
}
diff --git a/bskyweb/static.go b/bskyweb/static.go
index 38adb83335..4b59f27d76 100644
--- a/bskyweb/static.go
+++ b/bskyweb/static.go
@@ -2,7 +2,10 @@ package bskyweb
import "embed"
-//go:embed static/*
+// `all:` disables the default exclusion of files/dirs beginning with `_` or `.`,
+// which is needed because Metro emits chunks like `__common-...js` and
+// `__expo-metro-runtime-...js`.
+//go:embed all:static
var StaticFS embed.FS
//go:embed embedr-static/*
diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html
index 7960df859f..e5d8557039 100644
--- a/bskyweb/templates/base.html
+++ b/bskyweb/templates/base.html
@@ -13,7 +13,7 @@
-
+
+`
+console.log(`Writing ${fontsTemplateFile}`)
+fs.writeFileSync(fontsTemplateFile, fontsHtml)
+
+// Clean previous build output to avoid stale files
+function cleanDir(dir) {
+ if (fs.existsSync(dir)) {
+ fs.rmSync(dir, {recursive: true})
+ console.log(`Cleaned ${dir}`)
+ }
+}
+
+cleanDir(path.join(bskywebStatic, '_expo'))
+cleanDir(path.join(bskywebStatic, 'assets'))
+
+// Recursively copy a directory.
+function copyDir(sourceDir, targetDir) {
+ if (!fs.existsSync(sourceDir)) {
+ console.log(`Skipping ${sourceDir} (does not exist)`)
+ return
+ }
+ fs.mkdirSync(targetDir, {recursive: true})
+ const entries = fs.readdirSync(sourceDir, {withFileTypes: true})
+ for (const entry of entries) {
+ const sourcePath = path.join(sourceDir, entry.name)
+ const targetPath = path.join(targetDir, entry.name)
+ if (entry.isDirectory()) {
+ copyDir(sourcePath, targetPath)
+ } else {
+ fs.copyFileSync(sourcePath, targetPath)
+ console.log(`Copied ${sourcePath} to ${targetPath}`)
+ }
+ }
+}
+
+// Copy Metro's _expo/ tree wholesale so JS chunks (including lazy chunks
+// referenced by hash) and source maps resolve at the same paths the runtime expects.
+copyDir(path.join(distDir, '_expo'), path.join(bskywebStatic, '_expo'))
+
+// Copy assets (fonts, images, icons, etc.) referenced as /static/assets/... by the bundle.
+copyDir(path.join(distDir, 'assets'), path.join(bskywebStatic, 'assets'))
+
+// Upload source maps to Sentry
+if (process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_AUTH_TOKEN != 'unknown') {
+ const release =
+ process.env.SENTRY_RELEASE ||
+ require(path.join(projectRoot, 'package.json')).version
+ const dist = process.env.SENTRY_DIST || undefined
+ const sourceMapDir = path.join(distDir, '_expo', 'static')
+
+ console.log(`Uploading source maps to Sentry (release: ${release})`)
+ const args = [
+ 'sourcemaps',
+ 'upload',
+ '--org',
+ 'blueskyweb',
+ '--project',
+ 'app',
+ '--release',
+ release,
+ ...(dist ? ['--dist', dist] : []),
+ sourceMapDir,
+ ]
+ try {
+ execFileSync('sentry-cli', args, {stdio: 'inherit', cwd: projectRoot})
+ console.log('Sentry source map upload complete')
+ } catch (e) {
+ console.error('Sentry source map upload failed:', e.message)
+ process.exit(1)
+ }
+} else {
+ console.log('Skipping Sentry source map upload (SENTRY_AUTH_TOKEN not set)')
+}
\ No newline at end of file